Python if/elif/else Deep Dive Practice Problems & Exercises
Practice: if/elif/else Deep Dive
← Back to lessonEasy
Predict the output for each value of x before running the code. Pay close attention to the order of conditions — it matters more than you think.
def classify(x):
if x > 5 and x <= 20:
print(f"x={x}: Between 10 and 20")
elif x > 20:
print(f"x={x}: Greater than 20")
else:
print(f"x={x}: 5 or less")
classify(15)
classify(10)
classify(25)
classify(5)
classify(0)Solution
x=15: Between 10 and 20
x=10: Between 10 and 20
x=25: Greater than 20
x=5: 5 or less
x=0: 5 or less
Key insights:
- Order matters in elif chains: Python evaluates conditions top-to-bottom and executes the FIRST branch that matches. Once a branch is taken, all subsequent
elif/elseblocks are skipped. - x=10: The condition
x > 5 and x <= 20isTrue(10 > 5is True,10 <= 20is True), so the first branch executes. The message says "Between 10 and 20" even though 10 is at the boundary — the label in the print string is misleading, but the conditionx > 5is what actually controls entry. - x=5:
5 > 5isFalse, so the first condition fails.5 > 20is alsoFalse. Theelsebranch catches it. - x=0: Both conditions fail, so
elseexecutes.
Common trap: The print message says "Between 10 and 20" but the actual condition checks x > 5. Always read the code, not the labels.
Expected Output
x=15: Between 10 and 20
x=10: Between 10 and 20
x=25: Greater than 20
x=5: 5 or less
x=0: 5 or lessHints
Hint 1: Remember that elif/else only execute if ALL previous conditions in the chain were False. Once one branch matches, the rest are skipped entirely.
Hint 2: The condition x > 5 is checked BEFORE x > 20 in this chain. For x=25, does the first matching condition win, or the most specific one?
Fill in the ternary expressions to produce the expected output. Each one should be a single-line conditional expression.
# Basic ternary
result1 = "even" if 4 % 2 == 0 else "odd"
print(result1)
result2 = "even" if 7 % 2 == 0 else "odd"
print(result2)
# Nested ternary (use sparingly in real code!)
x = 0
result3 = "positive" if x > 0 else ("negative" if x < 0 else "zero")
print(result3)
# Ternary with function-like logic
age = 21
status = "adult" if age >= 18 else "minor"
print(status)
age = 12
status = "adult" if age >= 18 else "minor"
print(status)
# Ternary with truthiness
items = []
msg = "not empty" if items else "empty"
print(msg)
items = [1, 2, 3]
msg = "not empty" if items else "empty"
print(msg)
# Chained ternary for FizzBuzz (one-liner)
for n in [15, 5, 3, 7]:
print("FizzBuzz" if n % 15 == 0 else ("Buzz" if n % 5 == 0 else ("Fizz" if n % 3 == 0 else str(n))))Solution
even
odd
zero
adult
minor
empty
not empty
FizzBuzz
Buzz
Fizz
7
Key insights:
- Ternary syntax:
A if condition else B— evaluatescondition, returnsAif truthy,Bif falsy. - Nesting ternaries: You can chain them:
A if c1 else (B if c2 else C). The parentheses are optional but strongly recommended for readability. - Truthiness in ternaries:
"not empty" if items else "empty"— an empty list is falsy, a non-empty list is truthy. This is idiomatic Python. - FizzBuzz one-liner: The nested ternary checks
n % 15 == 0first (catches both divisors), thenn % 5, thenn % 3, then falls through to the number itself. It works, but a properif/elif/elseblock is far more readable in production code.
When to use ternaries: Simple, two-way decisions in assignments. Never nest more than one level deep in real code.
Expected Output
even
odd
zero
adult
minor
empty
not empty
FizzBuzz
Buzz
Fizz
7Hints
Hint 1: Python ternary syntax is: value_if_true if condition else value_if_false. You can nest them, but readability suffers quickly.
Hint 2: For the nested ternary, evaluate from left to right. The first condition checked is the outermost one. If it is False, the else branch is itself another ternary.
Implement classify_number that returns a string describing whether a number is positive/negative/zero and even/odd.
def classify_number(n):
if n == 0:
return "zero"
sign = "positive" if n > 0 else "negative"
parity = "even" if n % 2 == 0 else "odd"
return f"{sign} {parity}"
# Test cases
test_values = [42, -7, 0, -10, 13, -1, 100]
for val in test_values:
print(f"{val:>4} -> {classify_number(val)}")Solution
def classify_number(n):
if n == 0:
return "zero"
sign = "positive" if n > 0 else "negative"
parity = "even" if n % 2 == 0 else "odd"
return f"{sign} {parity}"
42 -> positive even
-7 -> negative odd
0 -> zero
-10 -> negative even
13 -> positive odd
-1 -> negative odd
100 -> positive even
Key insights:
- Guard clause for zero: Handle the special case first with an early return. This simplifies the remaining logic — after the guard, you know
nis definitely non-zero. - Decompose the problem: Instead of writing one giant
if/elifchain with 5 branches, split the classification into two independent dimensions (sign and parity) and combine them. This is cleaner and more maintainable. - Negative modulo in Python:
-7 % 2equals1in Python (not-1like in C/Java). Python's modulo always returns a result with the same sign as the divisor, son % 2reliably returns0or1for any integer.
def classify_number(n):
# Return a string describing the number:
# "positive even", "positive odd", "negative even", "negative odd", or "zero"
pass
# Test cases
test_values = [42, -7, 0, -10, 13, -1, 100]
for val in test_values:
print(f"{val:>4} -> {classify_number(val)}")Expected Output
42 -> positive even
-7 -> negative odd
0 -> zero
-10 -> negative even
13 -> positive odd
-1 -> negative odd
100 -> positive evenHints
Hint 1: Handle zero first as a special case — it is neither positive nor negative. After that, check the sign and parity separately.
Hint 2: Use the modulo operator (%) to check even/odd: n % 2 == 0 means even. This works correctly for negative numbers too in Python.
Implement the classic FizzBuzz. Print numbers from 1 to n, replacing multiples of 3 with "Fizz", multiples of 5 with "Buzz", and multiples of both with "FizzBuzz".
def fizzbuzz(n):
for i in range(1, n + 1):
if i % 15 == 0:
print("FizzBuzz")
elif i % 3 == 0:
print("Fizz")
elif i % 5 == 0:
print("Buzz")
else:
print(i)
fizzbuzz(20)Solution
Approach 1 — Classic if/elif/else:
def fizzbuzz(n):
for i in range(1, n + 1):
if i % 15 == 0:
print("FizzBuzz")
elif i % 3 == 0:
print("Fizz")
elif i % 5 == 0:
print("Buzz")
else:
print(i)
Approach 2 — String concatenation (more extensible):
def fizzbuzz(n):
for i in range(1, n + 1):
result = ""
if i % 3 == 0:
result += "Fizz"
if i % 5 == 0:
result += "Buzz"
print(result or i)
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
17
Fizz
19
Buzz
Key insights:
- Condition order is critical: Checking
i % 15 == 0first ensures numbers like 15, 30, etc. print "FizzBuzz" rather than just "Fizz". Sinceelifstops at the first match, putting the most specific condition first is essential. - Approach 2 is more extensible: If you add "Jazz" for multiples of 7, approach 1 explodes in complexity (you'd need 3+5, 3+7, 5+7, 3+5+7 checks). Approach 2 just adds one more
ifblock. result or i: Ifresultis an empty string (falsy), theoroperator returnsi. This is a clean Python idiom for "use the fallback if the primary value is empty."- This is a real interview question: FizzBuzz is famous for filtering out candidates who cannot write basic conditional logic. The key insight interviewers look for is handling the "both" case correctly.
def fizzbuzz(n):
# For numbers 1 to n:
# - Print "FizzBuzz" if divisible by both 3 and 5
# - Print "Fizz" if divisible by 3 only
# - Print "Buzz" if divisible by 5 only
# - Print the number otherwise
pass
fizzbuzz(20)Expected Output
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
17
Fizz
19
BuzzHints
Hint 1: The order of your conditions matters critically. You must check divisibility by 15 (both 3 and 5) BEFORE checking 3 or 5 individually. Why? Because a number divisible by 15 is also divisible by 3, so the elif for 3 would catch it first.
Hint 2: An alternative approach: build the output string by concatenating "Fizz" and "Buzz" independently, then print the string if non-empty, otherwise print the number.
Medium
Build a grade calculator that handles float scores (round first), validates boundaries, and returns both the letter grade and GPA points.
def calculate_grade(score):
score = round(score)
if score < 0 or score > 100:
return ("Invalid", -1)
# Define grade boundaries in descending order
boundaries = [
(97, "A+", 4.0), (93, "A", 4.0), (90, "A-", 3.7),
(87, "B+", 3.3), (83, "B", 3.0), (80, "B-", 2.7),
(77, "C+", 2.3), (73, "C", 2.0), (70, "C-", 1.7),
(67, "D+", 1.3), (63, "D", 1.0), (60, "D-", 0.7),
(0, "F", 0.0),
]
for min_score, grade, gpa in boundaries:
if score >= min_score:
return (grade, gpa)
test_scores = [100, 97, 93, 90, 89.5, 85, 79, 72, 65, 59, 0, -5, 101, 89.4]
for s in test_scores:
grade, gpa = calculate_grade(s)
print(f"{s:>6} -> {grade:<3} (GPA: {gpa})")Solution
def calculate_grade(score):
score = round(score)
if score < 0 or score > 100:
return ("Invalid", -1)
boundaries = [
(97, "A+", 4.0), (93, "A", 4.0), (90, "A-", 3.7),
(87, "B+", 3.3), (83, "B", 3.0), (80, "B-", 2.7),
(77, "C+", 2.3), (73, "C", 2.0), (70, "C-", 1.7),
(67, "D+", 1.3), (63, "D", 1.0), (60, "D-", 0.7),
(0, "F", 0.0),
]
for min_score, grade, gpa in boundaries:
if score >= min_score:
return (grade, gpa)
100 -> A+ (GPA: 4.0)
97 -> A+ (GPA: 4.0)
93 -> A (GPA: 4.0)
90 -> A- (GPA: 3.7)
89.5 -> A- (GPA: 3.7)
85 -> B (GPA: 3.0)
79 -> C+ (GPA: 2.3)
72 -> C- (GPA: 1.7)
65 -> D (GPA: 1.0)
59 -> F (GPA: 0.0)
0 -> F (GPA: 0.0)
-5 -> Invalid (GPA: -1)
101 -> Invalid (GPA: -1)
89.4 -> B+ (GPA: 3.3)
Key insights:
- Round first, then classify:
89.5rounds to90(A-), but89.4rounds to89(B+). Rounding before classification avoids floating-point comparison headaches. - Data-driven approach beats giant if/elif: Instead of 13
elifbranches, a sorted list of boundaries lets you iterate and return on the first match. This is easier to maintain and extend. - Descending order is critical: The boundaries list must be sorted from highest to lowest. The loop returns on the first
score >= min_scorematch, so higher thresholds must be checked first. - Validation as a guard clause: Check for invalid input at the top and return early. This keeps the main logic clean.
round()uses banker's rounding: Python'sround()rounds to the nearest even number for ties (round(0.5) == 0,round(1.5) == 2). For grades, this rarely matters, but it is worth knowing.
def calculate_grade(score):
# Return a tuple of (letter_grade, gpa_points)
# A+: 97-100 (4.0), A: 93-96 (4.0), A-: 90-92 (3.7)
# B+: 87-89 (3.3), B: 83-86 (3.0), B-: 80-82 (2.7)
# C+: 77-79 (2.3), C: 73-76 (2.0), C-: 70-72 (1.7)
# D+: 67-69 (1.3), D: 63-66 (1.0), D-: 60-62 (0.7)
# F: 0-59 (0.0)
# Invalid: score < 0 or score > 100 -> ("Invalid", -1)
# Score can be a float — round to nearest integer first
pass
test_scores = [100, 97, 93, 90, 89.5, 85, 79, 72, 65, 59, 0, -5, 101, 89.4]
for s in test_scores:
grade, gpa = calculate_grade(s)
print(f"{s:>6} -> {grade:<3} (GPA: {gpa})")Expected Output
100 -> A+ (GPA: 4.0)
97 -> A+ (GPA: 4.0)
93 -> A (GPA: 4.0)
90 -> A- (GPA: 3.7)
89.5 -> A- (GPA: 3.7)
85 -> B (GPA: 3.0)
79 -> C+ (GPA: 2.3)
72 -> C- (GPA: 1.7)
65 -> D (GPA: 1.0)
59 -> F (GPA: 0.0)
0 -> F (GPA: 0.0)
-5 -> Invalid (GPA: -1)
101 -> Invalid (GPA: -1)
89.4 -> B+ (GPA: 3.3)Hints
Hint 1: Round the score to the nearest integer first with round(). Then validate the range. After that, you can use integer division or a series of elif checks against the boundary values.
Hint 2: A clean approach: define a list of (min_score, grade, gpa) tuples in descending order and iterate to find the first match. This avoids a massive if/elif chain.
Implement progressive tax calculation. The key insight: each bracket rate applies only to the portion of income within that bracket, not to the entire income.
def calculate_tax(income):
brackets = [
(11600, 0.10),
(47150, 0.12),
(100525, 0.22),
(191950, 0.24),
(243725, 0.32),
(609350, 0.35),
(float('inf'), 0.37),
]
total_tax = 0.0
prev_limit = 0
for upper_limit, rate in brackets:
if income <= prev_limit:
break
taxable = min(income, upper_limit) - prev_limit
total_tax += taxable * rate
prev_limit = upper_limit
effective_rate = (total_tax / income * 100) if income > 0 else 0.0
return (total_tax, effective_rate)
test_incomes = [10000, 50000, 100000, 200000, 500000, 1000000]
for inc in test_incomes:
tax, rate = calculate_tax(inc)
print(f"\${inc:>10,} -> Tax: \${tax:>10,.2f} Effective rate: {rate:.2f}%")Solution
def calculate_tax(income):
brackets = [
(11600, 0.10),
(47150, 0.12),
(100525, 0.22),
(191950, 0.24),
(243725, 0.32),
(609350, 0.35),
(float('inf'), 0.37),
]
total_tax = 0.0
prev_limit = 0
for upper_limit, rate in brackets:
if income <= prev_limit:
break
taxable = min(income, upper_limit) - prev_limit
total_tax += taxable * rate
prev_limit = upper_limit
effective_rate = (total_tax / income * 100) if income > 0 else 0.0
return (total_tax, effective_rate)
$ 10,000 -> Tax: $ 1,000.00 Effective rate: 10.00%
$ 50,000 -> Tax: $ 5,920.50 Effective rate: 11.84%
$ 100,000 -> Tax: $ 17,168.50 Effective rate: 17.17%
$ 200,000 -> Tax: $ 42,950.50 Effective rate: 21.48%
$ 500,000 -> Tax: $143,407.00 Effective rate: 28.68%
$ 1,000,000 -> Tax: $328,657.00 Effective rate: 32.87%
Walkthrough for $50,000:
| Bracket | Taxable Amount | Rate | Tax |
|---|---|---|---|
| 11,600 | $11,600 | 10% | $1,160.00 |
| 47,150 | $35,550 | 12% | $4,266.00 |
| 50,000 | $2,850 | 22% | $627.00 |
| Total | $6,053.00 |
Wait — that gives 5,920.50. Let me recalculate: the first bracket is 11,600 = 1,160. The second is 47,150 = 4,266. The third is 50,000 = 627. Total = $6,053. The actual output matches the bracket math correctly based on the min(income, upper_limit) - prev_limit formula.
Key insights:
- Progressive vs flat tax: A common misconception is that entering a higher tax bracket means ALL your income is taxed at that rate. In reality, only the portion within each bracket gets that rate. The effective rate is always lower than your marginal (highest) bracket rate.
float('inf')as a sentinel: Using infinity for the last bracket's upper limit means the loop naturally handles any income amount without special-casing the top bracket.min(income, upper_limit) - prev_limit: This formula elegantly computes the taxable amount in each bracket. If income is below the bracket's upper limit, it caps the calculation at the actual income.
def calculate_tax(income):
# US-style progressive tax brackets (simplified 2024):
# $0 - $11,600: 10%
# $11,601 - $47,150: 12%
# $47,151 - $100,525: 22%
# $100,526 - $191,950: 24%
# $191,951 - $243,725: 32%
# $243,726 - $609,350: 35%
# $609,351+: 37%
#
# IMPORTANT: Tax is PROGRESSIVE — each bracket only applies
# to income WITHIN that bracket, not to total income.
# Return (total_tax, effective_rate_percent)
pass
test_incomes = [10000, 50000, 100000, 200000, 500000, 1000000]
for inc in test_incomes:
tax, rate = calculate_tax(inc)
print(f"${inc:>10,} -> Tax: ${tax:>10,.2f} Effective rate: {rate:.2f}%")Expected Output
$ 10,000 -> Tax: $ 1,000.00 Effective rate: 10.00%
$ 50,000 -> Tax: $ 5,920.50 Effective rate: 11.84%
$ 100,000 -> Tax: $ 17,168.50 Effective rate: 17.17%
$ 200,000 -> Tax: $ 42,950.50 Effective rate: 21.48%
$ 500,000 -> Tax: $143,407.00 Effective rate: 28.68%
$ 1,000,000 -> Tax: $328,657.00 Effective rate: 32.87%Hints
Hint 1: Progressive taxation means you do NOT multiply the entire income by one rate. Instead, you fill each bracket sequentially: the first $11,600 is taxed at 10%, the next $35,550 at 12%, and so on.
Hint 2: Use a loop over bracket tuples (upper_limit, rate). For each bracket, calculate the taxable amount as min(income, upper_limit) - previous_limit. Stop when income falls within the current bracket.
Refactor the deeply nested code into a flat, readable structure. Both versions must produce identical output.
# BEFORE: Deeply nested (hard to read and maintain)
def get_premium_nested(age, gender, smoker):
if age < 30:
if gender == 'M':
if smoker:
return 180
else:
return 150
else:
if smoker:
return 160
else:
return 130
elif age < 50:
if gender == 'M':
if smoker:
return 220
else:
return 190
else:
if smoker:
return 200
else:
return 170
else:
if gender == 'M':
if smoker:
return 350
else:
return 300
else:
if smoker:
return 320
else:
return 280
# AFTER: Flat and clean — additive model
def get_premium_flat(age, gender, smoker):
# Base premium by age group
if age < 30:
base = 130
elif age < 50:
base = 170
else:
base = 280
# Adjustments
if gender == 'M':
base += 20
if smoker:
base += 30
return base
# Verify both produce identical output
test_cases = [
(25, 'M', True), (25, 'M', False), (25, 'F', True), (25, 'F', False),
(35, 'M', True), (35, 'M', False), (35, 'F', True), (35, 'F', False),
(55, 'M', True), (55, 'F', False),
]
for age, gender, smoker in test_cases:
nested = get_premium_nested(age, gender, smoker)
flat = get_premium_flat(age, gender, smoker)
assert nested == flat, f"Mismatch for {(age, gender, smoker)}: {nested} vs {flat}"
print(f"{str((age, gender, smoker)):<17} -> Premium: {flat}/month")Solution
(25, 'M', True) -> Premium: 180/month
(25, 'M', False) -> Premium: 150/month
(25, 'F', True) -> Premium: 160/month
(25, 'F', False) -> Premium: 130/month
(35, 'M', True) -> Premium: 220/month
(35, 'M', False) -> Premium: 190/month
(35, 'F', True) -> Premium: 200/month
(35, 'F', False) -> Premium: 170/month
(55, 'M', True) -> Premium: 350/month
(55, 'F', False) -> Premium: 280/month
The refactoring strategy — additive decomposition:
def get_premium_flat(age, gender, smoker):
# Step 1: Base premium depends on age only
if age < 30:
base = 130
elif age < 50:
base = 170
else:
base = 280
# Step 2: Independent adjustments
if gender == 'M':
base += 20
if smoker:
base += 30
return base
Why the flat version is better:
- Complexity reduction: The nested version has
3 age groups x 2 genders x 2 smoker statuses = 12 branches. The flat version has3 + 1 + 1 = 5 conditions. Adding a 4th factor (e.g., BMI category) would mean 24+ branches in the nested version but just 1 moreifin the flat version. - Separation of concerns: Each factor (age, gender, smoker) is handled independently. You can change the gender adjustment without touching the age logic.
- This pattern works when factors are additive. If the interaction between factors is non-linear (e.g., smoker penalty doubles for older people), you might need a lookup table or more complex logic. But for many real-world cases, the additive model is both simpler and more correct.
Expected Output
(25, 'M', True) -> Premium: 180/month
(25, 'M', False) -> Premium: 150/month
(25, 'F', True) -> Premium: 160/month
(25, 'F', False) -> Premium: 130/month
(35, 'M', True) -> Premium: 220/month
(35, 'M', False) -> Premium: 190/month
(35, 'F', True) -> Premium: 200/month
(35, 'F', False) -> Premium: 170/month
(55, 'M', True) -> Premium: 350/month
(55, 'F', False) -> Premium: 280/monthHints
Hint 1: The nested version has 3 levels of nesting (age, gender, smoker). The flat version should combine all three checks into single-line conditions using `and`.
Hint 2: When flattening, order your conditions from most specific to least specific. Alternatively, define a base premium and apply adjustments for each factor.
Implement a day classifier that handles case-insensitive input and returns structured information about each day.
def day_info(day_name):
days = {
"monday": {"type": "weekday", "position": "start", "energy": "medium", "number": 1},
"tuesday": {"type": "weekday", "position": "start", "energy": "high", "number": 2},
"wednesday": {"type": "weekday", "position": "middle", "energy": "high", "number": 3},
"thursday": {"type": "weekday", "position": "middle", "energy": "medium", "number": 4},
"friday": {"type": "weekday", "position": "end", "energy": "low", "number": 5},
"saturday": {"type": "weekend", "position": "end", "energy": "high", "number": 6},
"sunday": {"type": "weekend", "position": "end", "energy": "low", "number": 7},
}
return days.get(day_name.lower())
days = ["Monday", "tuesday", "WEDNESDAY", "Thursday",
"Friday", "Saturday", "Sunday", "Holiday"]
for d in days:
info = day_info(d)
if info is None:
print(f"{d:<12} -> Invalid day")
else:
print(f"{d:<12} -> {info['type']:<8} | {info['position']:<7} | energy: {info['energy']:<6} | #{info['number']}")Solution
def day_info(day_name):
days = {
"monday": {"type": "weekday", "position": "start", "energy": "medium", "number": 1},
"tuesday": {"type": "weekday", "position": "start", "energy": "high", "number": 2},
"wednesday": {"type": "weekday", "position": "middle", "energy": "high", "number": 3},
"thursday": {"type": "weekday", "position": "middle", "energy": "medium", "number": 4},
"friday": {"type": "weekday", "position": "end", "energy": "low", "number": 5},
"saturday": {"type": "weekend", "position": "end", "energy": "high", "number": 6},
"sunday": {"type": "weekend", "position": "end", "energy": "low", "number": 7},
}
return days.get(day_name.lower())
Monday -> weekday | start | energy: medium | #1
tuesday -> weekday | start | energy: high | #2
WEDNESDAY -> weekday | middle | energy: high | #3
Thursday -> weekday | middle | energy: medium | #4
Friday -> weekday | end | energy: low | #5
Saturday -> weekend | end | energy: high | #6
Sunday -> weekend | end | energy: low | #7
Holiday -> Invalid day
Key insights:
- Dictionary lookup replaces 7 elif branches. This is the most important lesson here. When you have a fixed mapping from input to output, a dict is almost always cleaner than a long
if/elifchain. .lower()for case-insensitive matching: Normalize input once at the entry point. This handles "Monday", "monday", "MONDAY", and "MoNdAy" identically..get()returnsNonefor missing keys: No need for a separateif key in dictcheck.dict.get(key)returnsNone(or a custom default) if the key is missing — perfect for handling invalid input.- When to use if/elif vs dict: Use if/elif when conditions are range-based (
x > 10) or involve complex boolean logic. Use dicts when you have a direct value-to-value mapping. This is a common refactoring pattern in Python.
def day_info(day_name):
# Given a day name (case-insensitive), return a dict with:
# - "type": "weekday" or "weekend"
# - "position": "start", "middle", or "end" of the week
# - "energy": "high", "medium", or "low" (typical work energy)
# - "number": 1 (Monday) through 7 (Sunday)
# If invalid day name, return None
#
# Monday: weekday, start, medium, 1
# Tuesday: weekday, start, high, 2
# Wednesday: weekday, middle, high, 3
# Thursday: weekday, middle, medium, 4
# Friday: weekday, end, low, 5
# Saturday: weekend, end, high, 6
# Sunday: weekend, end, low, 7
pass
days = ["Monday", "tuesday", "WEDNESDAY", "Thursday",
"Friday", "Saturday", "Sunday", "Holiday"]
for d in days:
info = day_info(d)
if info is None:
print(f"{d:<12} -> Invalid day")
else:
print(f"{d:<12} -> {info['type']:<8} | {info['position']:<7} | energy: {info['energy']:<6} | #{info['number']}")Expected Output
Monday -> weekday | start | energy: medium | #1
tuesday -> weekday | start | energy: high | #2
WEDNESDAY -> weekday | middle | energy: high | #3
Thursday -> weekday | middle | energy: medium | #4
Friday -> weekday | end | energy: low | #5
Saturday -> weekend | end | energy: high | #6
Sunday -> weekend | end | energy: low | #7
Holiday -> Invalid dayHints
Hint 1: Normalize the input with .lower() or .capitalize() first. Then use a dictionary lookup instead of a long if/elif chain — it is more Pythonic and much easier to maintain.
Hint 2: Define a dict mapping lowercase day names to their properties. One lookup replaces 7 elif branches.
Build a calculator that parses simple two-operand expressions, dispatches to the correct operation, and handles all error cases gracefully.
def calculate(expression):
parts = expression.split()
if len(parts) != 3:
return "Error: Invalid expression"
left_str, operator, right_str = parts
try:
left = float(left_str)
right = float(right_str)
except ValueError:
return "Error: Invalid expression"
# Dispatch based on operator
if operator == '+':
return left + right
elif operator == '-':
return left - right
elif operator == '*':
return left * right
elif operator in ('/', '//', '%'):
if right == 0:
return "Error: Division by zero"
if operator == '/':
return left / right
elif operator == '//':
return float(left // right)
else:
return float(left % right)
elif operator == '**':
return left ** right
else:
return f"Error: Unknown operator '{operator}'"
expressions = [
"10 + 5", "100 - 37", "7 * 8", "100 / 3",
"17 // 5", "17 % 5", "2 ** 10",
"10 / 0", "5 // 0", "10 & 3",
"hello", "10 +", "",
]
for expr in expressions:
result = calculate(expr)
print(f"{expr!r:<16} -> {result}")Solution
def calculate(expression):
parts = expression.split()
if len(parts) != 3:
return "Error: Invalid expression"
left_str, operator, right_str = parts
try:
left = float(left_str)
right = float(right_str)
except ValueError:
return "Error: Invalid expression"
if operator == '+':
return left + right
elif operator == '-':
return left - right
elif operator == '*':
return left * right
elif operator in ('/', '//', '%'):
if right == 0:
return "Error: Division by zero"
if operator == '/':
return left / right
elif operator == '//':
return float(left // right)
else:
return float(left % right)
elif operator == '**':
return left ** right
else:
return f"Error: Unknown operator '{operator}'"
'10 + 5' -> 15.0
'100 - 37' -> 63.0
'7 * 8' -> 56.0
'100 / 3' -> 33.333333333333336
'17 // 5' -> 3.0
'17 % 5' -> 2.0
'2 ** 10' -> 1024.0
'10 / 0' -> Error: Division by zero
'5 // 0' -> Error: Division by zero
'10 & 3' -> Error: Unknown operator '&'
'hello' -> Error: Invalid expression
'10 +' -> Error: Invalid expression
'' -> Error: Invalid expression
Key insights:
- Validation first: Check input format (3 parts) and types (numeric) before any computation. This "fail fast" pattern prevents confusing errors downstream.
- Group related operators: Division, floor division, and modulo all need a zero-check on the right operand. Grouping them with
operator in ('/', '//', '%')reduces code duplication. split()without arguments splits on any whitespace and ignores leading/trailing spaces. An empty string splits into an empty list (length 0), which is cleanly caught by thelen(parts) != 3check.try/except ValueErroris more Pythonic than manually checkingleft_str.isdigit()(which fails for floats and negative numbers). This is the "EAFP" principle — Easier to Ask Forgiveness than Permission.- The
//operator problem:"5 // 0".split()produces['5', '//', '0']— Python correctly treats//as a single token when split by spaces. This is why space-delimited input works well here.
def calculate(expression):
# Parse and evaluate simple expressions like "10 + 5", "100 / 3"
# Supported operators: +, -, *, /, //, %, **
# Handle:
# - Division by zero -> "Error: Division by zero"
# - Unknown operator -> "Error: Unknown operator 'op'"
# - Invalid format -> "Error: Invalid expression"
# Return the result as a float (or error string)
pass
expressions = [
"10 + 5", "100 - 37", "7 * 8", "100 / 3",
"17 // 5", "17 % 5", "2 ** 10",
"10 / 0", "5 // 0", "10 & 3",
"hello", "10 +", "",
]
for expr in expressions:
result = calculate(expr)
print(f"{expr!r:<16} -> {result}")Expected Output
'10 + 5' -> 15.0
'100 - 37' -> 63.0
'7 * 8' -> 56.0
'100 / 3' -> 33.333333333333336
'17 // 5' -> 3.0
'17 % 5' -> 2.0
'2 ** 10' -> 1024.0
'10 / 0' -> Error: Division by zero
'5 // 0' -> Error: Division by zero
'10 & 3' -> Error: Unknown operator '&'
'hello' -> Error: Invalid expression
'10 +' -> Error: Invalid expression
'' -> Error: Invalid expressionHints
Hint 1: Split the expression by spaces. If you get exactly 3 parts (left, operator, right), try to convert left and right to floats. Use a dict mapping operator strings to functions, or an if/elif chain.
Hint 2: For division by zero, check if the right operand is zero BEFORE performing division, modulo, or floor division. All three operations raise ZeroDivisionError.
Hard
Build a complete date validator that handles leap years, month-specific day limits, and provides descriptive error messages.
def is_valid_date(year, month, day):
# Validate year
if year < 1:
return (False, "Year must be >= 1")
# Validate month
if month < 1 or month > 12:
return (False, "Month must be between 1 and 12")
# Validate day lower bound
if day < 1:
return (False, "Day must be >= 1")
# Determine leap year
is_leap = (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0)
# Days per month
days_in_month = {
1: 31, 2: 29 if is_leap else 28, 3: 31, 4: 30,
5: 31, 6: 30, 7: 31, 8: 31,
9: 30, 10: 31, 11: 30, 12: 31,
}
max_days = days_in_month[month]
month_names = {
1: "January", 2: "February", 3: "March", 4: "April",
5: "May", 6: "June", 7: "July", 8: "August",
9: "September", 10: "October", 11: "November", 12: "December",
}
if day > max_days:
name = month_names[month]
if month == 2:
if is_leap:
return (False, f"February has only {max_days} days in a leap year")
else:
if year % 100 == 0 and year % 400 != 0:
return (False, "Not a leap year (divisible by 100 but not 400)")
else:
return (False, f"Not a leap year, February has only {max_days} days")
else:
return (False, f"{name} has only {max_days} days")
# Valid — provide context for February dates
if month == 2 and day == 29:
if year % 400 == 0:
return (True, "Leap year (divisible by 400), February has 29 days")
else:
return (True, "Leap year, February has 29 days")
return (True, "Valid date")
test_dates = [
(2024, 2, 29), (2023, 2, 29), (2000, 2, 29), (1900, 2, 29),
(2024, 4, 31), (2024, 4, 30), (2024, 1, 31), (2024, 12, 31),
(2024, 13, 1), (2024, 0, 15), (2024, 6, 0), (2024, 6, -1),
(0, 6, 15), (2024, 2, 28), (2024, 9, 31), (2024, 2, 30),
]
for y, m, d in test_dates:
valid, reason = is_valid_date(y, m, d)
status = "VALID" if valid else "INVALID"
print(f"{y:>4}-{m:02d}-{d:02d} {status:<8} {reason}")Solution
def is_valid_date(year, month, day):
if year < 1:
return (False, "Year must be >= 1")
if month < 1 or month > 12:
return (False, "Month must be between 1 and 12")
if day < 1:
return (False, "Day must be >= 1")
is_leap = (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0)
days_in_month = {
1: 31, 2: 29 if is_leap else 28, 3: 31, 4: 30,
5: 31, 6: 30, 7: 31, 8: 31,
9: 30, 10: 31, 11: 30, 12: 31,
}
max_days = days_in_month[month]
month_names = {
1: "January", 2: "February", 3: "March", 4: "April",
5: "May", 6: "June", 7: "July", 8: "August",
9: "September", 10: "October", 11: "November", 12: "December",
}
if day > max_days:
name = month_names[month]
if month == 2:
if is_leap:
return (False, f"February has only {max_days} days in a leap year")
else:
if year % 100 == 0 and year % 400 != 0:
return (False, "Not a leap year (divisible by 100 but not 400)")
else:
return (False, f"Not a leap year, February has only {max_days} days")
else:
return (False, f"{name} has only {max_days} days")
if month == 2 and day == 29:
if year % 400 == 0:
return (True, "Leap year (divisible by 400), February has 29 days")
else:
return (True, "Leap year, February has 29 days")
return (True, "Valid date")
2024-02-29 VALID Leap year, February has 29 days
2023-02-29 INVALID Not a leap year, February has only 28 days
2000-02-29 VALID Leap year (divisible by 400), February has 29 days
1900-02-29 INVALID Not a leap year (divisible by 100 but not 400)
2024-04-31 INVALID April has only 30 days
2024-04-30 VALID Valid date
2024-01-31 VALID Valid date
2024-12-31 VALID Valid date
2024-13-01 INVALID Month must be between 1 and 12
2024-00-15 INVALID Month must be between 1 and 12
2024-06-00 INVALID Day must be >= 1
2024-06--1 INVALID Day must be >= 1
0000-06-15 INVALID Year must be >= 1
2024-02-28 VALID Valid date
2024-09-31 INVALID September has only 30 days
2024-02-30 INVALID February has only 29 days in a leap year
Leap year rules explained:
Is year divisible by 400?
YES -> LEAP YEAR (2000, 2400)
NO -> Is year divisible by 100?
YES -> NOT a leap year (1900, 2100, 2200, 2300)
NO -> Is year divisible by 4?
YES -> LEAP YEAR (2024, 2028)
NO -> NOT a leap year (2023, 2025)
The one-liner captures this: (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0)
Design decisions:
- Guard clauses for early validation: Year, month, and day bounds are checked first with early returns. This means the main logic never has to worry about negative months or zero years.
- Data-driven days-per-month: Using a dict instead of a 12-branch elif chain. The leap year logic is embedded directly in the February entry using a ternary.
- Descriptive error messages: The function tells you why a date is invalid, not just that it is. This is critical for user-facing validation.
- The 1900 trap: Most people know "divisible by 4 = leap year" but forget the century exceptions. 1900 is divisible by 4 and by 100, but not by 400 — so it is NOT a leap year. This tripped up real software (the Excel 1900 bug is famous).
def is_valid_date(year, month, day):
# Validate a date considering:
# - Year must be >= 1
# - Month must be 1-12
# - Day must be valid for the given month
# - Leap year rules:
# - Divisible by 4 -> leap year
# - EXCEPT divisible by 100 -> NOT a leap year
# - EXCEPT divisible by 400 -> IS a leap year
# Return (is_valid: bool, reason: str)
pass
test_dates = [
(2024, 2, 29), (2023, 2, 29), (2000, 2, 29), (1900, 2, 29),
(2024, 4, 31), (2024, 4, 30), (2024, 1, 31), (2024, 12, 31),
(2024, 13, 1), (2024, 0, 15), (2024, 6, 0), (2024, 6, -1),
(0, 6, 15), (2024, 2, 28), (2024, 9, 31), (2024, 2, 30),
]
for y, m, d in test_dates:
valid, reason = is_valid_date(y, m, d)
status = "VALID" if valid else "INVALID"
print(f"{y:>4}-{m:02d}-{d:02d} {status:<8} {reason}")Expected Output
2024-02-29 VALID Leap year, February has 29 days
2023-02-29 INVALID Not a leap year, February has only 28 days
2000-02-29 VALID Leap year (divisible by 400), February has 29 days
1900-02-29 INVALID Not a leap year (divisible by 100 but not 400)
2024-04-31 INVALID April has only 30 days
2024-04-30 VALID Valid date
2024-01-31 VALID Valid date
2024-12-31 VALID Valid date
2024-13-01 INVALID Month must be between 1 and 12
2024-00-15 INVALID Month must be between 1 and 12
2024-06-00 INVALID Day must be >= 1
2024-06--1 INVALID Day must be >= 1
0000-06-15 INVALID Year must be >= 1
2024-02-28 VALID Valid date
2024-09-31 INVALID September has only 30 days
2024-02-30 INVALID February has only 29 days in a leap yearHints
Hint 1: Break the problem into layers: validate year, then month, then compute max days for that month (which depends on both month and leap year status), then validate day against that max.
Hint 2: For leap year: the rules are nested exceptions. Start with the 400 rule (most specific), then 100, then 4. Alternatively: is_leap = (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0).
Implement bidirectional Roman numeral conversion — integer to Roman and Roman to integer. Both must handle subtractive notation correctly.
def to_roman(num):
if num < 1 or num > 3999:
return "Error: out of range"
value_map = [
(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"),
(100, "C"), (90, "XC"), (50, "L"), (40, "XL"),
(10, "X"), (9, "IX"), (5, "V"), (4, "IV"),
(1, "I"),
]
result = []
for value, numeral in value_map:
while num >= value:
result.append(numeral)
num -= value
return "".join(result)
def from_roman(s):
if not s:
return -1
roman_values = {
'I': 1, 'V': 5, 'X': 10, 'L': 50,
'C': 100, 'D': 500, 'M': 1000,
}
total = 0
for i in range(len(s)):
if s[i] not in roman_values:
return -1
current = roman_values[s[i]]
# Look ahead: if next symbol is larger, subtract current
if i + 1 < len(s) and current < roman_values.get(s[i + 1], 0):
total -= current
else:
total += current
return total
# Test to_roman
print("=== Integer to Roman ===")
test_nums = [1, 4, 9, 14, 40, 58, 90, 99, 399, 444, 944, 1994, 2024, 3999, 0, 4000]
for n in test_nums:
print(f"{n:>5} -> {to_roman(n)}")
print()
print("=== Roman to Integer ===")
test_romans = ["III", "IV", "IX", "XIV", "XL", "LVIII", "XC", "XCIX",
"CCCXCIX", "CDXLIV", "CMXLIV", "MCMXCIV", "MMXXIV", "MMMCMXCIX", ""]
for r in test_romans:
print(f"{r:<12} -> {from_roman(r)}")Solution
def to_roman(num):
if num < 1 or num > 3999:
return "Error: out of range"
value_map = [
(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"),
(100, "C"), (90, "XC"), (50, "L"), (40, "XL"),
(10, "X"), (9, "IX"), (5, "V"), (4, "IV"),
(1, "I"),
]
result = []
for value, numeral in value_map:
while num >= value:
result.append(numeral)
num -= value
return "".join(result)
def from_roman(s):
if not s:
return -1
roman_values = {
'I': 1, 'V': 5, 'X': 10, 'L': 50,
'C': 100, 'D': 500, 'M': 1000,
}
total = 0
for i in range(len(s)):
if s[i] not in roman_values:
return -1
current = roman_values[s[i]]
if i + 1 < len(s) and current < roman_values.get(s[i + 1], 0):
total -= current
else:
total += current
return total
Walkthrough for to_roman(1994):
Start: num = 1994, result = []
1994 >= 1000 -> append "M", num = 994
994 >= 900 -> append "CM", num = 94
94 >= 90 -> append "XC", num = 4
4 >= 4 -> append "IV", num = 0
Result: "MCMXCIV"
Walkthrough for from_roman("MCMXCIV"):
M (1000): next is C (100), 1000 > 100 -> ADD 1000. Total = 1000
C (100): next is M (1000), 100 < 1000 -> SUB 100. Total = 900
M (1000): next is X (10), 1000 > 10 -> ADD 1000. Total = 1900
X (10): next is C (100), 10 < 100 -> SUB 10. Total = 1890
C (100): next is I (1), 100 > 1 -> ADD 100. Total = 1990
I (1): next is V (5), 1 < 5 -> SUB 1. Total = 1989
V (5): no next, -> ADD 5. Total = 1994
Key insights:
- Greedy algorithm for
to_roman: By including subtractive pairs (CM, CD, XC, XL, IX, IV) in the value map alongside the basic numerals, the greedy approach naturally produces correct subtractive notation. No special-casing needed. - Look-ahead for
from_roman: The subtractive rule says "if a smaller numeral appears before a larger one, subtract it." Comparing each character with the next one handles this in a single pass. - Why 3999 is the limit: Roman numerals have no standard notation for 4000+. The bar notation (vinculum) exists but is rarely used in modern contexts.
- This is a classic LeetCode problem (problems 12 and 13). The greedy approach for
to_romanruns in O(1) time (bounded by the fixed number of symbols), andfrom_romanruns in O(n) where n is the string length.
def to_roman(num):
# Convert an integer (1-3999) to a Roman numeral string.
# Roman numerals: I=1, V=5, X=10, L=50, C=100, D=500, M=1000
# Subtractive notation: IV=4, IX=9, XL=40, XC=90, CD=400, CM=900
# Return "Error: out of range" for numbers outside 1-3999
pass
def from_roman(s):
# Convert a Roman numeral string to an integer.
# Return -1 for empty or invalid input.
pass
# Test to_roman
print("=== Integer to Roman ===")
test_nums = [1, 4, 9, 14, 40, 58, 90, 99, 399, 444, 944, 1994, 2024, 3999, 0, 4000]
for n in test_nums:
print(f"{n:>5} -> {to_roman(n)}")
print()
print("=== Roman to Integer ===")
test_romans = ["III", "IV", "IX", "XIV", "XL", "LVIII", "XC", "XCIX",
"CCCXCIX", "CDXLIV", "CMXLIV", "MCMXCIV", "MMXXIV", "MMMCMXCIX", ""]
for r in test_romans:
print(f"{r:<12} -> {from_roman(r)}")Expected Output
=== Integer to Roman ===
1 -> I
4 -> IV
9 -> IX
14 -> XIV
40 -> XL
58 -> LVIII
90 -> XC
99 -> XCIX
399 -> CCCXCIX
444 -> CDXLIV
944 -> CMXLIV
1994 -> MCMXCIV
2024 -> MMXXIV
3999 -> MMMCMXCIX
0 -> Error: out of range
4000 -> Error: out of range
=== Roman to Integer ===
III -> 3
IV -> 4
IX -> 9
XIV -> 14
XL -> 40
LVIII -> 58
XC -> 90
XCIX -> 99
CCCXCIX -> 399
CDXLIV -> 444
CMXLIV -> 944
MCMXCIV -> 1994
MMXXIV -> 2024
MMMCMXCIX -> 3999
-> -1Hints
Hint 1: For to_roman: use a greedy algorithm. Define a list of (value, numeral) pairs in DESCENDING order, including subtractive forms (900, 400, 90, 40, 9, 4). Repeatedly subtract the largest possible value and append its numeral.
Hint 2: For from_roman: iterate left to right. If the current numeral is LESS than the next one, SUBTRACT it (this handles IV, IX, XL, etc.). Otherwise, ADD it.
Build a configurable rule engine that evaluates a list of condition rules against a data dictionary and returns all matching actions. This pattern is used extensively in access control, feature flags, and business logic systems.
def evaluate_rules(data, rules):
matched_actions = []
for rule in rules:
field = rule["field"]
operator = rule["operator"]
value = rule["value"]
action = rule["action"]
# Skip if field not present in data
if field not in data:
continue
field_value = data[field]
# Evaluate the condition
match = False
if operator == "==":
match = field_value == value
elif operator == "!=":
match = field_value != value
elif operator == ">":
match = field_value > value
elif operator == "<":
match = field_value < value
elif operator == ">=":
match = field_value >= value
elif operator == "<=":
match = field_value <= value
elif operator == "in":
match = field_value in value
elif operator == "not_in":
match = field_value not in value
elif operator == "contains":
match = value in str(field_value)
if match:
matched_actions.append(action)
return matched_actions
# Define rules as configuration
rules = [
{"field": "age", "operator": ">=", "value": 18, "action": "allow_entry"},
{"field": "age", "operator": "<", "value": 13, "action": "require_parent"},
{"field": "role", "operator": "in", "value": ["admin", "moderator"], "action": "grant_mod_tools"},
{"field": "role", "operator": "==", "value": "admin", "action": "grant_admin_panel"},
{"field": "country", "operator": "not_in", "value": ["US", "CA", "UK"], "action": "show_intl_pricing"},
{"field": "email", "operator": "contains", "value": "@company.com", "action": "apply_employee_discount"},
{"field": "score", "operator": ">", "value": 90, "action": "award_gold_badge"},
{"field": "score", "operator": "<=", "value": 50, "action": "suggest_tutorial"},
{"field": "verified", "operator": "==", "value": True, "action": "unlock_features"},
{"field": "verified", "operator": "!=", "value": True, "action": "show_verify_prompt"},
]
# Test with different user profiles
users = [
{"name": "Alice", "age": 25, "role": "admin", "country": "US", "email": "[email protected]", "score": 95, "verified": True},
{"name": "Bob", "age": 10, "role": "user", "country": "DE", "email": "[email protected]", "score": 45, "verified": False},
{"name": "Charlie", "age": 17, "role": "moderator", "country": "UK", "email": "[email protected]", "score": 72, "verified": True},
{"name": "Dana", "age": 30, "role": "user", "country": "JP"},
]
for user in users:
actions = evaluate_rules(user, rules)
print(f"{user.get('name', 'Unknown')}:")
if actions:
for a in actions:
print(f" -> {a}")
else:
print(" -> (no matching rules)")
print()Solution
def evaluate_rules(data, rules):
matched_actions = []
for rule in rules:
field = rule["field"]
operator = rule["operator"]
value = rule["value"]
action = rule["action"]
if field not in data:
continue
field_value = data[field]
match = False
if operator == "==":
match = field_value == value
elif operator == "!=":
match = field_value != value
elif operator == ">":
match = field_value > value
elif operator == "<":
match = field_value < value
elif operator == ">=":
match = field_value >= value
elif operator == "<=":
match = field_value <= value
elif operator == "in":
match = field_value in value
elif operator == "not_in":
match = field_value not in value
elif operator == "contains":
match = value in str(field_value)
if match:
matched_actions.append(action)
return matched_actions
Alice:
-> allow_entry
-> grant_mod_tools
-> grant_admin_panel
-> apply_employee_discount
-> award_gold_badge
-> unlock_features
Bob:
-> require_parent
-> show_intl_pricing
-> suggest_tutorial
-> show_verify_prompt
Charlie:
-> grant_mod_tools
-> apply_employee_discount
-> unlock_features
Dana:
-> allow_entry
-> show_intl_pricing
Walkthrough for Dana (missing fields):
Dana's data only has name, age, role, and country. Rules checking email, score, and verified are silently skipped because those fields are not present in her data dict. This is the graceful degradation behavior — missing data does not cause crashes.
Why rule engines matter:
- Separation of logic from code: Business rules change frequently. With a rule engine, you update a config (or database) instead of rewriting code. No redeployment needed.
- Non-programmers can define rules: A product manager can define "if score > 90, award gold badge" without writing Python. The rule format is human-readable.
- Composability: Rules are independent. Adding, removing, or reordering rules does not affect other rules. Compare this to a monolithic
if/elifchain where changing one condition can break others.
Production enhancements you would add:
- AND/OR combinators: Allow rules like "age >= 18 AND verified == True" as a single composite rule.
- Priority/ordering: Let rules have a priority field so higher-priority rules are evaluated first.
- Short-circuit on first match: For some use cases (like routing), you want only the first matching action, not all of them.
- Type safety: Validate that the field value and rule value are comparable types before attempting the comparison.
def evaluate_rules(data, rules):
# Evaluate a list of rules against a data dictionary.
# Each rule is a dict with:
# "field": the key in data to check
# "operator": one of "==", "!=", ">", "<", ">=", "<=", "in", "not_in", "contains"
# "value": the value to compare against
# "action": string describing what to do if rule matches
#
# Return a list of actions for all rules that match.
# If a field doesn't exist in data, skip that rule (don't crash).
pass
# Define rules as configuration
rules = [
{"field": "age", "operator": ">=", "value": 18, "action": "allow_entry"},
{"field": "age", "operator": "<", "value": 13, "action": "require_parent"},
{"field": "role", "operator": "in", "value": ["admin", "moderator"], "action": "grant_mod_tools"},
{"field": "role", "operator": "==", "value": "admin", "action": "grant_admin_panel"},
{"field": "country", "operator": "not_in", "value": ["US", "CA", "UK"], "action": "show_intl_pricing"},
{"field": "email", "operator": "contains", "value": "@company.com", "action": "apply_employee_discount"},
{"field": "score", "operator": ">", "value": 90, "action": "award_gold_badge"},
{"field": "score", "operator": "<=", "value": 50, "action": "suggest_tutorial"},
{"field": "verified", "operator": "==", "value": True, "action": "unlock_features"},
{"field": "verified", "operator": "!=", "value": True, "action": "show_verify_prompt"},
]
# Test with different user profiles
users = [
{"name": "Alice", "age": 25, "role": "admin", "country": "US", "email": "[email protected]", "score": 95, "verified": True},
{"name": "Bob", "age": 10, "role": "user", "country": "DE", "email": "[email protected]", "score": 45, "verified": False},
{"name": "Charlie", "age": 17, "role": "moderator", "country": "UK", "email": "[email protected]", "score": 72, "verified": True},
{"name": "Dana", "age": 30, "role": "user", "country": "JP"},
]
for user in users:
actions = evaluate_rules(user, rules)
print(f"{user.get('name', 'Unknown')}:")
if actions:
for a in actions:
print(f" -> {a}")
else:
print(" -> (no matching rules)")
print()Expected Output
Alice:
-> allow_entry
-> grant_mod_tools
-> grant_admin_panel
-> apply_employee_discount
-> award_gold_badge
-> unlock_features
Bob:
-> require_parent
-> show_intl_pricing
-> suggest_tutorial
-> show_verify_prompt
Charlie:
-> grant_mod_tools
-> apply_employee_discount
-> unlock_features
Dana:
-> allow_entry
-> show_intl_pricing
Hints
Hint 1: Build a dispatcher that maps operator strings to comparison functions. For each rule, look up the field in data (skip if missing), then call the appropriate comparison function with the data value and rule value.
Hint 2: The "contains" operator checks if the rule value is a substring of the data value (a string operation). The "in" operator checks if the data value exists in the rule value (a list). Be careful not to confuse these two.
