← Computer Programming II

Variant 1: Online Exam Grader

You are building an online exam grading system. The system stores students, lets them submit answers, and calculates scores by comparing answers against an answer key. It must raise meaningful custom exceptions when things go wrong.

  1. Create a base exception ExamError that inherits from Exception.
  2. Create StudentAlreadyRegisteredError inheriting from ExamError. Its __init__ takes a name parameter, stores it as an attribute, and passes the message "student already registered: {name}" to the parent.
  3. Create StudentNotRegisteredError inheriting from ExamError. Its __init__ takes a name parameter, stores it as an attribute, and passes the message "student not registered: {name}" to the parent.
  4. Create InvalidAnswerError inheriting from ExamError. Its __init__ takes question_num and valid_options parameters, stores both as attributes, and passes the message "invalid answer for question {question_num}. valid options: {valid_options}" to the parent.
  5. Create an ExamGrader class with:
    • __init__(self, answer_key) — takes a dictionary mapping question numbers (int) to correct answers (str), e.g., {1: "B", 2: "A", 3: "C"}. Stores the answer key and initializes an empty dictionary for student submissions. Hint: student submissions should be stored as a nested dictionary: {student_name: {question_num: answer}}, e.g., {"Dana": {1: "B", 2: "A"}, "Emir": {}}.
    • register_student(self, name) — registers a student. Raises StudentAlreadyRegisteredError if the student is already registered. Otherwise stores an empty dictionary ({}) for their answers (this inner dict will later hold {question_num: answer} pairs).
    • submit_answer(self, name, question_num, answer) — records a student’s answer for a question. Use EAFP style (try/except KeyError) to check if the student is registered; if not, raise StudentNotRegisteredError using from None. Raises InvalidAnswerError if question_num is not in the answer key (pass the list of valid question numbers as valid_options). Stores the answer for the student.
    • grade(self, name) — calculates and returns the student’s percentage score as an integer. Use EAFP style to check if the student is registered. Compares each submitted answer against the answer key. Only questions the student answered are graded. The score formula is: correct answers / total questions in answer key * 100, rounded down to an integer. If the student submitted no answers, return 0.

Input

key = {1: "B", 2: "A", 3: "C", 4: "D"}
grader = ExamGrader(key)

grader.register_student("Dana")
grader.register_student("Emir")

grader.submit_answer("Dana", 1, "B")
grader.submit_answer("Dana", 2, "A")
grader.submit_answer("Dana", 3, "B")
grader.submit_answer("Dana", 4, "D")

grader.submit_answer("Emir", 1, "B")
grader.submit_answer("Emir", 2, "C")

print(f"Dana: {grader.grade('Dana')}%")
print(f"Emir: {grader.grade('Emir')}%")

tests = [
    lambda: grader.register_student("Dana"),
    lambda: grader.submit_answer("Zara", 1, "A"),
    lambda: grader.submit_answer("Emir", 7, "A"),
]

for test in tests:
    try:
        test()
    except ExamError as e:
        print(e)

Expected Output

Dana: 75%
Emir: 25%
student already registered: Dana
student not registered: Zara
invalid answer for question 7. valid options: [1, 2, 3, 4]

Variant 2: Warehouse Inventory Manager

You are building an inventory system for a warehouse. The system tracks products with their quantities and prices, supports restocking and selling, and computes the total inventory value. It must raise meaningful custom exceptions for invalid operations.

  1. Create a base exception InventoryError that inherits from Exception.
  2. Create ProductNotFoundError inheriting from InventoryError. Its __init__ takes a product_name parameter, stores it as an attribute, and passes the message "product not found: {product_name}" to the parent.
  3. Create InsufficientStockError inheriting from InventoryError. Its __init__ takes product_name, requested, and available parameters, stores all three as attributes, computes self.shortage = requested - available, and passes the message "cannot sell {requested} of {product_name}: only {available} in stock, short by {shortage}" to the parent.
  4. Create InvalidQuantityError inheriting from InventoryError. Its __init__ takes a quantity parameter, stores it as an attribute, and passes the message "invalid quantity: {quantity}. must be positive" to the parent.
  5. Create a Warehouse class with:
    • __init__(self) — initializes an empty dictionary for products. Hint: products should be stored as a nested dictionary: {product_name: {"price": float, "quantity": int}}, e.g., {"Laptop": {"price": 899.99, "quantity": 10}}.
    • add_product(self, name, price, quantity) — adds a new product or increases quantity if it already exists. Raises InvalidQuantityError if quantity is not positive. If the product is new, stores its price and quantity as a dict {"price": price, "quantity": quantity}. If it exists, adds the quantity to the current stock and updates the price to the new value.
    • sell(self, name, quantity) — reduces the stock of a product. Raises InvalidQuantityError if quantity is not positive. Uses EAFP style (try/except KeyError) to look up the product; if not found, raises ProductNotFoundError using from None. Raises InsufficientStockError if the requested quantity exceeds available stock. Returns the total sale price as quantity * price, rounded to 2 decimal places.
    • total_value(self) — returns the total value of all products in stock as sum of (price * quantity) for each product, rounded to 2 decimal places.

Input

wh = Warehouse()

wh.add_product("Laptop", 899.99, 10)
wh.add_product("Mouse", 25.50, 50)
wh.add_product("Keyboard", 45.00, 30)

print(f"total value: {wh.total_value()}")

sale = wh.sell("Laptop", 3)
print(f"sold 3 laptops for: {sale}")
print(f"total value: {wh.total_value()}")

wh.add_product("Mouse", 27.00, 20)
print(f"total value: {wh.total_value()}")

tests = [
    lambda: wh.sell("Monitor", 1),
    lambda: wh.sell("Laptop", 50),
    lambda: wh.add_product("Tablet", 199.99, -5),
]

for test in tests:
    try:
        test()
    except InventoryError as e:
        print(e)

Expected Output

total value: 11624.9
sold 3 laptops for: 2699.97
total value: 8924.93
total value: 9539.93
product not found: Monitor
cannot sell 50 of Laptop: only 7 in stock, short by 43
invalid quantity: -5. must be positive

Variant 3: Recipe Book Manager

You are building a recipe management system. The system stores recipes with their ingredients and required portions, can scale recipes to a desired number of servings, and checks if a recipe can be made with available pantry ingredients. It must raise meaningful custom exceptions for invalid operations.

  1. Create a base exception RecipeError that inherits from Exception.
  2. Create RecipeNotFoundError inheriting from RecipeError. Its __init__ takes a recipe_name parameter, stores it as an attribute, and passes the message "recipe not found: {recipe_name}" to the parent.
  3. Create DuplicateRecipeError inheriting from RecipeError. Its __init__ takes a recipe_name parameter, stores it as an attribute, and passes the message "recipe already exists: {recipe_name}" to the parent.
  4. Create InvalidServingsError inheriting from RecipeError. Its __init__ takes a servings parameter, stores it as an attribute, and passes the message "invalid servings: {servings}. must be positive" to the parent.
  5. Create MissingIngredientsError inheriting from RecipeError. Its __init__ takes a recipe_name and missing (a dictionary of ingredient name to amount short) parameter, stores both as attributes, and passes the message "cannot make {recipe_name}: missing {missing}" to the parent.
  6. Create a RecipeBook class with:
    • __init__(self) — initializes an empty dictionary for recipes. Hint: recipes should be stored as a nested dictionary: {recipe_name: {"servings": int, "ingredients": {name: float}}}, e.g., {"Pancakes": {"servings": 4, "ingredients": {"flour": 2.0, "eggs": 3.0}}}.
    • add_recipe(self, name, servings, ingredients) — adds a recipe. servings is the base number of servings (int). ingredients is a dictionary mapping ingredient names to amounts needed (floats), e.g., {"flour": 2.0, "eggs": 3.0}. Raises DuplicateRecipeError if the recipe already exists. Raises InvalidServingsError if servings is not positive. Stores the recipe data as {"servings": servings, "ingredients": ingredients}.
    • scale_recipe(self, name, desired_servings) — returns a new dictionary of ingredients scaled to the desired number of servings. Uses EAFP style (try/except KeyError) to look up the recipe; if not found, raises RecipeNotFoundError using from None. Raises InvalidServingsError if desired_servings is not positive. The formula is: ingredient_amount * (desired_servings / base_servings), each rounded to 2 decimal places.
    • check_pantry(self, name, pantry) — checks if a recipe (for its base servings) can be made with the given pantry (a dictionary of ingredient name to available amount). Uses EAFP for recipe lookup. If any ingredients are missing or insufficient, raises MissingIngredientsError with a dictionary of each lacking ingredient and the amount short (rounded to 2 decimal places). If all ingredients are sufficient, returns True.

Input

book = RecipeBook()

book.add_recipe("Pancakes", 4, {"flour": 2.0, "eggs": 3.0, "milk": 1.5, "sugar": 0.5})
book.add_recipe("Omelette", 2, {"eggs": 4.0, "cheese": 1.0, "pepper": 0.25})

scaled = book.scale_recipe("Pancakes", 8)
print(f"pancakes for 8: {scaled}")

scaled = book.scale_recipe("Omelette", 1)
print(f"omelette for 1: {scaled}")

pantry = {"flour": 2.0, "eggs": 1.0, "milk": 1.5, "sugar": 0.5}
try:
    book.check_pantry("Pancakes", pantry)
except RecipeError as e:
    print(e)

pantry2 = {"eggs": 5.0, "cheese": 2.0, "pepper": 1.0}
result = book.check_pantry("Omelette", pantry2)
print(f"can make omelette: {result}")

tests = [
    lambda: book.add_recipe("Pancakes", 4, {"flour": 1.0}),
    lambda: book.scale_recipe("Salad", 2),
    lambda: book.scale_recipe("Pancakes", -1),
]

for test in tests:
    try:
        test()
    except RecipeError as e:
        print(e)

Expected Output

pancakes for 8: {'flour': 4.0, 'eggs': 6.0, 'milk': 3.0, 'sugar': 1.0}
omelette for 1: {'eggs': 2.0, 'cheese': 0.5, 'pepper': 0.12}
cannot make Pancakes: missing {'eggs': 2.0}
can make omelette: True
recipe already exists: Pancakes
recipe not found: Salad
invalid servings: -1. must be positive

Variant 4: Driving Test Evaluator

You are building a driving test evaluation system. The system registers candidates, lets them submit answers to traffic rule questions, and calculates pass/fail scores by comparing answers against a correct answer sheet. It must raise meaningful custom exceptions when things go wrong.

  1. Create a base exception TestError that inherits from Exception.
  2. Create CandidateAlreadyRegisteredError inheriting from TestError. Its __init__ takes a name parameter, stores it as an attribute, and passes the message "candidate already registered: {name}" to the parent.
  3. Create CandidateNotRegisteredError inheriting from TestError. Its __init__ takes a name parameter, stores it as an attribute, and passes the message "candidate not registered: {name}" to the parent.
  4. Create InvalidQuestionError inheriting from TestError. Its __init__ takes question_num and valid_options parameters, stores both as attributes, and passes the message "invalid question number {question_num}. valid questions: {valid_options}" to the parent.
  5. Create a DrivingTestEvaluator class with:
    • __init__(self, answer_sheet) — takes a dictionary mapping question numbers (int) to correct answers (str), e.g., {1: "C", 2: "A", 3: "B"}. Stores the answer sheet and initializes an empty dictionary for candidate submissions. Hint: candidate submissions should be stored as a nested dictionary: {candidate_name: {question_num: answer}}, e.g., {"Amir": {1: "C", 2: "A"}, "Lola": {}}.
    • register_candidate(self, name) — registers a candidate. Raises CandidateAlreadyRegisteredError if the candidate is already registered. Otherwise stores an empty dictionary ({}) for their answers (this inner dict will later hold {question_num: answer} pairs).
    • submit_answer(self, name, question_num, answer) — records a candidate’s answer for a question. Use EAFP style (try/except KeyError) to check if the candidate is registered; if not, raise CandidateNotRegisteredError using from None. Raises InvalidQuestionError if question_num is not in the answer sheet (pass the list of valid question numbers as valid_options). Stores the answer for the candidate.
    • evaluate(self, name) — calculates and returns the candidate’s percentage score as an integer. Use EAFP style to check if the candidate is registered. Compares each submitted answer against the answer sheet. Only questions the candidate answered are evaluated. The score formula is: correct answers / total questions in answer sheet * 100, rounded down to an integer. If the candidate submitted no answers, return 0.

Input

sheet = {1: "C", 2: "A", 3: "B", 4: "D", 5: "A"}
evaluator = DrivingTestEvaluator(sheet)

evaluator.register_candidate("Amir")
evaluator.register_candidate("Lola")

evaluator.submit_answer("Amir", 1, "C")
evaluator.submit_answer("Amir", 2, "A")
evaluator.submit_answer("Amir", 3, "B")
evaluator.submit_answer("Amir", 4, "D")
evaluator.submit_answer("Amir", 5, "A")

evaluator.submit_answer("Lola", 1, "C")
evaluator.submit_answer("Lola", 2, "B")
evaluator.submit_answer("Lola", 3, "A")

print(f"Amir: {evaluator.evaluate('Amir')}%")
print(f"Lola: {evaluator.evaluate('Lola')}%")

tests = [
    lambda: evaluator.register_candidate("Amir"),
    lambda: evaluator.submit_answer("Kamol", 1, "A"),
    lambda: evaluator.submit_answer("Lola", 9, "B"),
]

for test in tests:
    try:
        test()
    except TestError as e:
        print(e)

Expected Output

Amir: 100%
Lola: 20%
candidate already registered: Amir
candidate not registered: Kamol
invalid question number 9. valid questions: [1, 2, 3, 4, 5]

Variant 5: Bookstore Inventory Manager

You are building an inventory system for a bookstore. The system tracks books with their quantities and prices, supports restocking and selling, and computes the total inventory value. It must raise meaningful custom exceptions for invalid operations.

  1. Create a base exception BookstoreError that inherits from Exception.
  2. Create BookNotFoundError inheriting from BookstoreError. Its __init__ takes a book_title parameter, stores it as an attribute, and passes the message "book not found: {book_title}" to the parent.
  3. Create InsufficientCopiesError inheriting from BookstoreError. Its __init__ takes book_title, requested, and available parameters, stores all three as attributes, computes self.shortage = requested - available, and passes the message "cannot sell {requested} of {book_title}: only {available} in stock, short by {shortage}" to the parent.
  4. Create InvalidQuantityError inheriting from BookstoreError. Its __init__ takes a quantity parameter, stores it as an attribute, and passes the message "invalid quantity: {quantity}. must be positive" to the parent.
  5. Create a Bookstore class with:
    • __init__(self) — initializes an empty dictionary for books. Hint: books should be stored as a nested dictionary: {book_title: {"price": float, "quantity": int}}, e.g., {"Python Basics": {"price": 35.50, "quantity": 20}}.
    • add_book(self, title, price, quantity) — adds a new book or increases quantity if it already exists. Raises InvalidQuantityError if quantity is not positive. If the book is new, stores its price and quantity as a dict {"price": price, "quantity": quantity}. If it exists, adds the quantity to the current stock and updates the price to the new value.
    • sell(self, title, quantity) — reduces the stock of a book. Raises InvalidQuantityError if quantity is not positive. Uses EAFP style (try/except KeyError) to look up the book; if not found, raises BookNotFoundError using from None. Raises InsufficientCopiesError if the requested quantity exceeds available stock. Returns the total sale price as quantity * price, rounded to 2 decimal places.
    • total_value(self) — returns the total value of all books in stock as sum of (price * quantity) for each book, rounded to 2 decimal places.

Input

store = Bookstore()

store.add_book("Python Basics", 35.50, 20)
store.add_book("Data Science", 49.99, 15)
store.add_book("Algorithms", 62.00, 8)

print(f"total value: {store.total_value()}")

sale = store.sell("Python Basics", 5)
print(f"sold 5 copies for: {sale}")
print(f"total value: {store.total_value()}")

store.add_book("Data Science", 52.99, 10)
print(f"total value: {store.total_value()}")

tests = [
    lambda: store.sell("Machine Learning", 1),
    lambda: store.sell("Algorithms", 20),
    lambda: store.add_book("Web Dev", 29.99, -3),
]

for test in tests:
    try:
        test()
    except BookstoreError as e:
        print(e)

Expected Output

total value: 1955.85
sold 5 copies for: 177.5
total value: 1778.35
total value: 2353.25
book not found: Machine Learning
cannot sell 20 of Algorithms: only 8 in stock, short by 12
invalid quantity: -3. must be positive

Variant 6: Cocktail Menu Manager

You are building a cocktail menu management system. The system stores cocktails with their ingredients and base serving sizes, can scale cocktails to a desired number of servings, and checks if a cocktail can be made with available bar stock. It must raise meaningful custom exceptions for invalid operations.

  1. Create a base exception CocktailError that inherits from Exception.
  2. Create CocktailNotFoundError inheriting from CocktailError. Its __init__ takes a cocktail_name parameter, stores it as an attribute, and passes the message "cocktail not found: {cocktail_name}" to the parent.
  3. Create DuplicateCocktailError inheriting from CocktailError. Its __init__ takes a cocktail_name parameter, stores it as an attribute, and passes the message "cocktail already exists: {cocktail_name}" to the parent.
  4. Create InvalidServingsError inheriting from CocktailError. Its __init__ takes a servings parameter, stores it as an attribute, and passes the message "invalid servings: {servings}. must be positive" to the parent.
  5. Create MissingStockError inheriting from CocktailError. Its __init__ takes a cocktail_name and missing (a dictionary of ingredient name to amount short) parameter, stores both as attributes, and passes the message "cannot make {cocktail_name}: missing {missing}" to the parent.
  6. Create a CocktailMenu class with:
    • __init__(self) — initializes an empty dictionary for cocktails. Hint: cocktails should be stored as a nested dictionary: {cocktail_name: {"servings": int, "ingredients": {name: float}}}, e.g., {"Mojito": {"servings": 2, "ingredients": {"rum": 3.0, "lime": 2.0}}}.
    • add_cocktail(self, name, servings, ingredients) — adds a cocktail. servings is the base number of servings (int). ingredients is a dictionary mapping ingredient names to amounts needed (floats), e.g., {"rum": 2.0, "lime": 1.0}. Raises DuplicateCocktailError if the cocktail already exists. Raises InvalidServingsError if servings is not positive. Stores the cocktail data as {"servings": servings, "ingredients": ingredients}.
    • scale_cocktail(self, name, desired_servings) — returns a new dictionary of ingredients scaled to the desired number of servings. Uses EAFP style (try/except KeyError) to look up the cocktail; if not found, raises CocktailNotFoundError using from None. Raises InvalidServingsError if desired_servings is not positive. The formula is: ingredient_amount * (desired_servings / base_servings), each rounded to 2 decimal places.
    • check_stock(self, name, bar_stock) — checks if a cocktail (for its base servings) can be made with the given bar stock (a dictionary of ingredient name to available amount). Uses EAFP for cocktail lookup. If any ingredients are missing or insufficient, raises MissingStockError with a dictionary of each lacking ingredient and the amount short (rounded to 2 decimal places). If all ingredients are sufficient, returns True.

Input

menu = CocktailMenu()

menu.add_cocktail("Mojito", 2, {"rum": 3.0, "lime": 2.0, "mint": 1.0, "sugar": 0.5})
menu.add_cocktail("Margarita", 3, {"tequila": 4.5, "lime": 3.0, "salt": 0.75})

scaled = menu.scale_cocktail("Mojito", 6)
print(f"mojito for 6: {scaled}")

scaled = menu.scale_cocktail("Margarita", 1)
print(f"margarita for 1: {scaled}")

bar = {"rum": 3.0, "lime": 0.5, "mint": 1.0, "sugar": 0.5}
try:
    menu.check_stock("Mojito", bar)
except CocktailError as e:
    print(e)

bar2 = {"tequila": 10.0, "lime": 5.0, "salt": 2.0}
result = menu.check_stock("Margarita", bar2)
print(f"can make margarita: {result}")

tests = [
    lambda: menu.add_cocktail("Mojito", 2, {"rum": 1.0}),
    lambda: menu.scale_cocktail("Daiquiri", 4),
    lambda: menu.scale_cocktail("Mojito", -2),
]

for test in tests:
    try:
        test()
    except CocktailError as e:
        print(e)

Expected Output

mojito for 6: {'rum': 9.0, 'lime': 6.0, 'mint': 3.0, 'sugar': 1.5}
margarita for 1: {'tequila': 1.5, 'lime': 1.0, 'salt': 0.25}
cannot make Mojito: missing {'lime': 1.5}
can make margarita: True
cocktail already exists: Mojito
cocktail not found: Daiquiri
invalid servings: -2. must be positive

Variant 7: Job Interview Scorer

You are building a job interview scoring system. The system registers applicants, lets interviewers record scores for predefined skill categories, and calculates overall performance percentages. It must raise meaningful custom exceptions when things go wrong.

  1. Create a base exception InterviewError that inherits from Exception.
  2. Create ApplicantAlreadyRegisteredError inheriting from InterviewError. Its __init__ takes a name parameter, stores it as an attribute, and passes the message "applicant already registered: {name}" to the parent.
  3. Create ApplicantNotRegisteredError inheriting from InterviewError. Its __init__ takes a name parameter, stores it as an attribute, and passes the message "applicant not registered: {name}" to the parent.
  4. Create InvalidCategoryError inheriting from InterviewError. Its __init__ takes category and valid_categories parameters, stores both as attributes, and passes the message "invalid category {category}. valid categories: {valid_categories}" to the parent.
  5. Create an InterviewScorer class with:
    • __init__(self, max_scores) — takes a dictionary mapping category names (str) to maximum possible scores (int), e.g., {"python": 20, "sql": 15, "communication": 10}. Stores the max scores and initializes an empty dictionary for applicant submissions. Hint: applicant submissions should be stored as a nested dictionary: {applicant_name: {category: score}}, e.g., {"Nodira": {"python": 18, "sql": 12}, "Rustam": {}}.
    • register_applicant(self, name) — registers an applicant. Raises ApplicantAlreadyRegisteredError if the applicant is already registered. Otherwise stores an empty dictionary ({}) for their scores (this inner dict will later hold {category: score} pairs).
    • record_score(self, name, category, score) — records an applicant’s score for a category. Use EAFP style (try/except KeyError) to check if the applicant is registered; if not, raise ApplicantNotRegisteredError using from None. Raises InvalidCategoryError if category is not in the max scores dictionary (pass the list of valid category names as valid_categories). Stores the score for the applicant.
    • evaluate(self, name) — calculates and returns the applicant’s percentage score as an integer. Use EAFP style to check if the applicant is registered. Compares each recorded score against the max possible for that category. The score formula is: sum of recorded scores / sum of all max scores * 100, rounded down to an integer. If the applicant has no recorded scores, return 0.

Input

categories = {"python": 20, "sql": 15, "communication": 10, "problem_solving": 25}
scorer = InterviewScorer(categories)

scorer.register_applicant("Nodira")
scorer.register_applicant("Rustam")

scorer.record_score("Nodira", "python", 18)
scorer.record_score("Nodira", "sql", 12)
scorer.record_score("Nodira", "communication", 9)
scorer.record_score("Nodira", "problem_solving", 20)

scorer.record_score("Rustam", "python", 10)
scorer.record_score("Rustam", "sql", 5)

print(f"Nodira: {scorer.evaluate('Nodira')}%")
print(f"Rustam: {scorer.evaluate('Rustam')}%")

tests = [
    lambda: scorer.register_applicant("Nodira"),
    lambda: scorer.record_score("Temur", "python", 15),
    lambda: scorer.record_score("Rustam", "java", 10),
]

for test in tests:
    try:
        test()
    except InterviewError as e:
        print(e)

Expected Output

Nodira: 84%
Rustam: 21%
applicant already registered: Nodira
applicant not registered: Temur
invalid category java. valid categories: ['python', 'sql', 'communication', 'problem_solving']

Variant 8: Pharmacy Stock Manager

You are building a stock management system for a pharmacy. The system tracks medicines with their quantities and prices, supports restocking and dispensing, and computes the total stock value. It must raise meaningful custom exceptions for invalid operations.

  1. Create a base exception PharmacyError that inherits from Exception.
  2. Create MedicineNotFoundError inheriting from PharmacyError. Its __init__ takes a medicine_name parameter, stores it as an attribute, and passes the message "medicine not found: {medicine_name}" to the parent.
  3. Create InsufficientSupplyError inheriting from PharmacyError. Its __init__ takes medicine_name, requested, and available parameters, stores all three as attributes, computes self.shortage = requested - available, and passes the message "cannot dispense {requested} of {medicine_name}: only {available} in stock, short by {shortage}" to the parent.
  4. Create InvalidQuantityError inheriting from PharmacyError. Its __init__ takes a quantity parameter, stores it as an attribute, and passes the message "invalid quantity: {quantity}. must be positive" to the parent.
  5. Create a Pharmacy class with:
    • __init__(self) — initializes an empty dictionary for medicines. Hint: medicines should be stored as a nested dictionary: {medicine_name: {"price": float, "quantity": int}}, e.g., {"Aspirin": {"price": 5.99, "quantity": 100}}.
    • add_medicine(self, name, price, quantity) — adds a new medicine or increases quantity if it already exists. Raises InvalidQuantityError if quantity is not positive. If the medicine is new, stores its price and quantity as a dict {"price": price, "quantity": quantity}. If it exists, adds the quantity to the current stock and updates the price to the new value.
    • dispense(self, name, quantity) — reduces the stock of a medicine. Raises InvalidQuantityError if quantity is not positive. Uses EAFP style (try/except KeyError) to look up the medicine; if not found, raises MedicineNotFoundError using from None. Raises InsufficientSupplyError if the requested quantity exceeds available stock. Returns the total cost as quantity * price, rounded to 2 decimal places.
    • total_value(self) — returns the total value of all medicines in stock as sum of (price * quantity) for each medicine, rounded to 2 decimal places.

Input

ph = Pharmacy()

ph.add_medicine("Aspirin", 5.99, 100)
ph.add_medicine("Insulin", 120.50, 30)
ph.add_medicine("Amoxicillin", 12.75, 60)

print(f"total value: {ph.total_value()}")

cost = ph.dispense("Insulin", 5)
print(f"dispensed 5 insulin for: {cost}")
print(f"total value: {ph.total_value()}")

ph.add_medicine("Aspirin", 6.49, 50)
print(f"total value: {ph.total_value()}")

tests = [
    lambda: ph.dispense("Vitamin D", 10),
    lambda: ph.dispense("Amoxicillin", 100),
    lambda: ph.add_medicine("Paracetamol", 3.99, -20),
]

for test in tests:
    try:
        test()
    except PharmacyError as e:
        print(e)

Expected Output

total value: 4979.0
dispensed 5 insulin for: 602.5
total value: 4376.5
total value: 4751.0
medicine not found: Vitamin D
cannot dispense 100 of Amoxicillin: only 60 in stock, short by 40
invalid quantity: -20. must be positive

Variant 9: Paint Mixer

You are building a paint mixing system. The system stores paint formulas with their pigment components and base batch sizes, can scale formulas to a desired batch count, and checks if a formula can be mixed with available pigment supplies. It must raise meaningful custom exceptions for invalid operations.

  1. Create a base exception PaintError that inherits from Exception.
  2. Create FormulaNotFoundError inheriting from PaintError. Its __init__ takes a formula_name parameter, stores it as an attribute, and passes the message "formula not found: {formula_name}" to the parent.
  3. Create DuplicateFormulaError inheriting from PaintError. Its __init__ takes a formula_name parameter, stores it as an attribute, and passes the message "formula already exists: {formula_name}" to the parent.
  4. Create InvalidBatchError inheriting from PaintError. Its __init__ takes a batches parameter, stores it as an attribute, and passes the message "invalid batches: {batches}. must be positive" to the parent.
  5. Create MissingPigmentsError inheriting from PaintError. Its __init__ takes a formula_name and missing (a dictionary of pigment name to amount short) parameter, stores both as attributes, and passes the message "cannot mix {formula_name}: missing {missing}" to the parent.
  6. Create a PaintMixer class with:
    • __init__(self) — initializes an empty dictionary for formulas. Hint: formulas should be stored as a nested dictionary: {formula_name: {"batches": int, "pigments": {name: float}}}, e.g., {"Sunset Orange": {"batches": 2, "pigments": {"red": 4.0, "yellow": 3.0}}}.
    • add_formula(self, name, batches, pigments) — adds a formula. batches is the base number of batches (int). pigments is a dictionary mapping pigment names to amounts needed (floats), e.g., {"red": 3.0, "white": 5.0}. Raises DuplicateFormulaError if the formula already exists. Raises InvalidBatchError if batches is not positive. Stores the formula data as {"batches": batches, "pigments": pigments}.
    • scale_formula(self, name, desired_batches) — returns a new dictionary of pigments scaled to the desired number of batches. Uses EAFP style (try/except KeyError) to look up the formula; if not found, raises FormulaNotFoundError using from None. Raises InvalidBatchError if desired_batches is not positive. The formula is: pigment_amount * (desired_batches / base_batches), each rounded to 2 decimal places.
    • check_supplies(self, name, supplies) — checks if a formula (for its base batches) can be mixed with the given supplies (a dictionary of pigment name to available amount). Uses EAFP for formula lookup. If any pigments are missing or insufficient, raises MissingPigmentsError with a dictionary of each lacking pigment and the amount short (rounded to 2 decimal places). If all pigments are sufficient, returns True.

Input

mixer = PaintMixer()

mixer.add_formula("Sunset Orange", 2, {"red": 4.0, "yellow": 3.0, "white": 1.0})
mixer.add_formula("Ocean Blue", 3, {"blue": 6.0, "white": 3.0, "green": 0.75})

scaled = mixer.scale_formula("Sunset Orange", 6)
print(f"sunset orange for 6: {scaled}")

scaled = mixer.scale_formula("Ocean Blue", 1)
print(f"ocean blue for 1: {scaled}")

supplies = {"red": 4.0, "yellow": 1.0, "white": 1.0}
try:
    mixer.check_supplies("Sunset Orange", supplies)
except PaintError as e:
    print(e)

supplies2 = {"blue": 10.0, "white": 5.0, "green": 2.0}
result = mixer.check_supplies("Ocean Blue", supplies2)
print(f"can mix ocean blue: {result}")

tests = [
    lambda: mixer.add_formula("Sunset Orange", 2, {"red": 1.0}),
    lambda: mixer.scale_formula("Forest Green", 3),
    lambda: mixer.scale_formula("Ocean Blue", -4),
]

for test in tests:
    try:
        test()
    except PaintError as e:
        print(e)

Expected Output

sunset orange for 6: {'red': 12.0, 'yellow': 9.0, 'white': 3.0}
ocean blue for 1: {'blue': 2.0, 'white': 1.0, 'green': 0.25}
cannot mix Sunset Orange: missing {'yellow': 2.0}
can mix ocean blue: True
formula already exists: Sunset Orange
formula not found: Forest Green
invalid batches: -4. must be positive

Variant 10: Quiz Tournament Judge

You are building a quiz tournament judging system. The system registers teams, lets them submit answers to numbered rounds, and calculates final standings by comparing answers against the official answers. It must raise meaningful custom exceptions when things go wrong.

  1. Create a base exception TournamentError that inherits from Exception.
  2. Create TeamAlreadyRegisteredError inheriting from TournamentError. Its __init__ takes a team_name parameter, stores it as an attribute, and passes the message "team already registered: {team_name}" to the parent.
  3. Create TeamNotRegisteredError inheriting from TournamentError. Its __init__ takes a team_name parameter, stores it as an attribute, and passes the message "team not registered: {team_name}" to the parent.
  4. Create InvalidRoundError inheriting from TournamentError. Its __init__ takes round_num and valid_rounds parameters, stores both as attributes, and passes the message "invalid round {round_num}. valid rounds: {valid_rounds}" to the parent.
  5. Create a QuizJudge class with:
    • __init__(self, official_answers) — takes a dictionary mapping round numbers (int) to correct answers (str), e.g., {1: "Paris", 2: "7", 3: "Mars"}. Stores the official answers and initializes an empty dictionary for team submissions. Hint: team submissions should be stored as a nested dictionary: {team_name: {round_num: answer}}, e.g., {"Wolves": {1: "Paris", 2: "7"}, "Eagles": {}}.
    • register_team(self, team_name) — registers a team. Raises TeamAlreadyRegisteredError if the team is already registered. Otherwise stores an empty dictionary ({}) for their answers (this inner dict will later hold {round_num: answer} pairs).
    • submit_answer(self, team_name, round_num, answer) — records a team’s answer for a round. Use EAFP style (try/except KeyError) to check if the team is registered; if not, raise TeamNotRegisteredError using from None. Raises InvalidRoundError if round_num is not in the official answers (pass the list of valid round numbers as valid_rounds). Stores the answer for the team.
    • score(self, team_name) — calculates and returns the team’s percentage score as an integer. Use EAFP style to check if the team is registered. Compares each submitted answer against the official answers. Only rounds the team answered are scored. The score formula is: correct answers / total rounds in official answers * 100, rounded down to an integer. If the team submitted no answers, return 0.

Input

answers = {1: "Paris", 2: "7", 3: "Mars", 4: "Einstein", 5: "1945", 6: "Au"}
judge = QuizJudge(answers)

judge.register_team("Wolves")
judge.register_team("Eagles")

judge.submit_answer("Wolves", 1, "Paris")
judge.submit_answer("Wolves", 2, "7")
judge.submit_answer("Wolves", 3, "Jupiter")
judge.submit_answer("Wolves", 4, "Einstein")
judge.submit_answer("Wolves", 5, "1945")
judge.submit_answer("Wolves", 6, "Au")

judge.submit_answer("Eagles", 1, "London")
judge.submit_answer("Eagles", 2, "7")
judge.submit_answer("Eagles", 3, "Mars")

print(f"Wolves: {judge.score('Wolves')}%")
print(f"Eagles: {judge.score('Eagles')}%")

tests = [
    lambda: judge.register_team("Wolves"),
    lambda: judge.submit_answer("Foxes", 1, "Paris"),
    lambda: judge.submit_answer("Eagles", 10, "answer"),
]

for test in tests:
    try:
        test()
    except TournamentError as e:
        print(e)

Expected Output

Wolves: 83%
Eagles: 33%
team already registered: Wolves
team not registered: Foxes
invalid round 10. valid rounds: [1, 2, 3, 4, 5, 6]

Variant 11: Restaurant Supply Manager

You are building a supply management system for a restaurant. The system tracks ingredients with their quantities and costs, supports restocking and using ingredients for orders, and computes the total supply value. It must raise meaningful custom exceptions for invalid operations.

  1. Create a base exception SupplyError that inherits from Exception.
  2. Create IngredientNotFoundError inheriting from SupplyError. Its __init__ takes an ingredient_name parameter, stores it as an attribute, and passes the message "ingredient not found: {ingredient_name}" to the parent.
  3. Create InsufficientIngredientError inheriting from SupplyError. Its __init__ takes ingredient_name, requested, and available parameters, stores all three as attributes, computes self.shortage = requested - available, and passes the message "cannot use {requested} of {ingredient_name}: only {available} in stock, short by {shortage}" to the parent.
  4. Create InvalidQuantityError inheriting from SupplyError. Its __init__ takes a quantity parameter, stores it as an attribute, and passes the message "invalid quantity: {quantity}. must be positive" to the parent.
  5. Create a Restaurant class with:
    • __init__(self) — initializes an empty dictionary for ingredients. Hint: ingredients should be stored as a nested dictionary: {ingredient_name: {"cost": float, "quantity": int}}, e.g., {"Olive Oil": {"cost": 8.75, "quantity": 40}}.
    • add_ingredient(self, name, cost, quantity) — adds a new ingredient or increases quantity if it already exists. Raises InvalidQuantityError if quantity is not positive. If the ingredient is new, stores its cost and quantity as a dict {"cost": cost, "quantity": quantity}. If it exists, adds the quantity to the current stock and updates the cost to the new value.
    • use(self, name, quantity) — reduces the stock of an ingredient. Raises InvalidQuantityError if quantity is not positive. Uses EAFP style (try/except KeyError) to look up the ingredient; if not found, raises IngredientNotFoundError using from None. Raises InsufficientIngredientError if the requested quantity exceeds available stock. Returns the total cost as quantity * cost, rounded to 2 decimal places.
    • total_value(self) — returns the total value of all ingredients in stock as sum of (cost * quantity) for each ingredient, rounded to 2 decimal places.

Input

rest = Restaurant()

rest.add_ingredient("Olive Oil", 8.75, 40)
rest.add_ingredient("Flour", 2.30, 200)
rest.add_ingredient("Butter", 4.50, 80)

print(f"total value: {rest.total_value()}")

cost = rest.use("Flour", 50)
print(f"used 50 flour for: {cost}")
print(f"total value: {rest.total_value()}")

rest.add_ingredient("Olive Oil", 9.25, 30)
print(f"total value: {rest.total_value()}")

tests = [
    lambda: rest.use("Saffron", 5),
    lambda: rest.use("Butter", 100),
    lambda: rest.add_ingredient("Salt", 1.50, -10),
]

for test in tests:
    try:
        test()
    except SupplyError as e:
        print(e)

Expected Output

total value: 1170.0
used 50 flour for: 115.0
total value: 1055.0
total value: 1352.5
ingredient not found: Saffron
cannot use 100 of Butter: only 80 in stock, short by 20
invalid quantity: -10. must be positive

Variant 12: Fertilizer Blend Planner

You are building a fertilizer blending system. The system stores blend formulas with their mineral components and base plot coverage, can scale formulas to a desired number of plots, and checks if a blend can be prepared with available mineral stock. It must raise meaningful custom exceptions for invalid operations.

  1. Create a base exception BlendError that inherits from Exception.
  2. Create BlendNotFoundError inheriting from BlendError. Its __init__ takes a blend_name parameter, stores it as an attribute, and passes the message "blend not found: {blend_name}" to the parent.
  3. Create DuplicateBlendError inheriting from BlendError. Its __init__ takes a blend_name parameter, stores it as an attribute, and passes the message "blend already exists: {blend_name}" to the parent.
  4. Create InvalidPlotsError inheriting from BlendError. Its __init__ takes a plots parameter, stores it as an attribute, and passes the message "invalid plots: {plots}. must be positive" to the parent.
  5. Create MissingMineralsError inheriting from BlendError. Its __init__ takes a blend_name and missing (a dictionary of mineral name to amount short) parameter, stores both as attributes, and passes the message "cannot prepare {blend_name}: missing {missing}" to the parent.
  6. Create a BlendPlanner class with:
    • __init__(self) — initializes an empty dictionary for blends. Hint: blends should be stored as a nested dictionary: {blend_name: {"plots": int, "minerals": {name: float}}}, e.g., {"Growth Mix": {"plots": 5, "minerals": {"nitrogen": 10.0, "phosphorus": 4.0}}}.
    • add_blend(self, name, plots, minerals) — adds a blend. plots is the base number of plots (int). minerals is a dictionary mapping mineral names to amounts needed (floats), e.g., {"nitrogen": 5.0, "phosphorus": 3.0}. Raises DuplicateBlendError if the blend already exists. Raises InvalidPlotsError if plots is not positive. Stores the blend data as {"plots": plots, "minerals": minerals}.
    • scale_blend(self, name, desired_plots) — returns a new dictionary of minerals scaled to the desired number of plots. Uses EAFP style (try/except KeyError) to look up the blend; if not found, raises BlendNotFoundError using from None. Raises InvalidPlotsError if desired_plots is not positive. The formula is: mineral_amount * (desired_plots / base_plots), each rounded to 2 decimal places.
    • check_stock(self, name, stock) — checks if a blend (for its base plots) can be prepared with the given stock (a dictionary of mineral name to available amount). Uses EAFP for blend lookup. If any minerals are missing or insufficient, raises MissingMineralsError with a dictionary of each lacking mineral and the amount short (rounded to 2 decimal places). If all minerals are sufficient, returns True.

Input

planner = BlendPlanner()

planner.add_blend("Growth Mix", 5, {"nitrogen": 10.0, "phosphorus": 4.0, "potassium": 6.0})
planner.add_blend("Bloom Boost", 3, {"phosphorus": 9.0, "potassium": 3.0, "iron": 1.5})

scaled = planner.scale_blend("Growth Mix", 10)
print(f"growth mix for 10: {scaled}")

scaled = planner.scale_blend("Bloom Boost", 1)
print(f"bloom boost for 1: {scaled}")

stock = {"nitrogen": 10.0, "phosphorus": 1.0, "potassium": 6.0}
try:
    planner.check_stock("Growth Mix", stock)
except BlendError as e:
    print(e)

stock2 = {"phosphorus": 15.0, "potassium": 5.0, "iron": 3.0}
result = planner.check_stock("Bloom Boost", stock2)
print(f"can prepare bloom boost: {result}")

tests = [
    lambda: planner.add_blend("Growth Mix", 5, {"nitrogen": 2.0}),
    lambda: planner.scale_blend("Root Strong", 4),
    lambda: planner.scale_blend("Growth Mix", -3),
]

for test in tests:
    try:
        test()
    except BlendError as e:
        print(e)

Expected Output

growth mix for 10: {'nitrogen': 20.0, 'phosphorus': 8.0, 'potassium': 12.0}
bloom boost for 1: {'phosphorus': 3.0, 'potassium': 1.0, 'iron': 0.5}
cannot prepare Growth Mix: missing {'phosphorus': 3.0}
can prepare bloom boost: True
blend already exists: Growth Mix
blend not found: Root Strong
invalid plots: -3. must be positive