Start learning free

LESSON 561 OF 569 · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

Payment Fees in Python: Abstract Classes with ABC

Practice O(1) Read the full explanation

A shop takes payments by card, by PayPal and in cash. Every payment has an amount, in cents, and a total: the amount plus a processing fee. The total is worked out the same way for every payment; only the fee depends on the kind of payment. That is what an abstract class is for: it holds the shared part, says that every payment has a fee, and leaves the fee itself to each kind of payment.

  • The abstract class Payment is built from an amount, in cents, and keeps it. Its method fee() is abstract: every payment has a fee, but Payment can't say what it is. Its method total() is concrete: it returns the amount plus fee().
  • Card, PayPal and Cash inherit from Payment, keep its total() as it is, and each writes its own fee():
  • Card pays 2%: its fee is amount × 2 / 100.
  • PayPal pays 3% plus 30 cents: its fee is amount × 3 / 100 + 30.
  • Cash costs nothing: its fee is 0.

In Python, Payment inherits from ABC (from the abc module) and marks fee with @abstractmethod, as in the abstract-classes lesson. Card, PayPal and Cash inherit __init__ and total and write only fee. Divide with //: it drops the fraction.

Every division rounds down: a card payment of 1999 cents has a fee of 39, not 39.98.

The starter code already has the import, every class and every method header: declare the abstract method and write the bodies. For each test, the harness creates the payments in the order given and prints two lines: each payment's total, total(), separated by spaces, and then the sum of all the fees. It calls them through the abstract type Payment, never through Card, PayPal or Cash.

 

Example 1:

Input:
    Card(1999)
    PayPal(500)
    Cash(700)
Output:
    2038 545 700
    84
Explanation: The card's fee is 1999 × 2 / 100 = 39 (39.98 rounded down), so its total is 2038. PayPal's fee is 500 × 3 / 100 + 30 = 45, total 545. Cash has no fee: total 700. The fees add up to 39 + 45 + 0 = 84.

Example 2:

Input:
    Card(49)
    PayPal(33)
    Card(50)
Output:
    49 63 51
    31
Explanation: 49 × 2 / 100 is 0.98, rounded down to 0. PayPal's 33 × 3 / 100 is 0.99, rounded down to 0, plus 30. The second card's 50 × 2 / 100 is exactly 1. The fees are 0 + 30 + 1 = 31.

Example 3:

Input:
    Card(0)
    PayPal(0)
    Cash(0)
Output:
    0 30 0
    30
Explanation: Amounts can be 0. PayPal still adds its 30 cents.

 

Constraints:

  • Every test has between 1 and 100 payments.
  • Every amount is an integer from 0 to 106 cents.

Note:

Every fee and every total takes O(1) time.

STEP 1 OF 6
    Ctrl + Enter to check

    PYTHON · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

    Payment Fees in Python, explained

    Problem overview

    A shop takes payments by card, by PayPal and in cash. Every payment has an amount in cents and a total(), which is the amount plus a processing fee. The total is worked out the same way for every payment. Only the fee depends on the kind of payment:

    • Card: amount * 2 / 100, rounded down.
    • PayPal: amount * 3 / 100 + 30, with the division rounded down.
    • Cash: 0.

    For Card(1999) the fee is 39 (the exact value 39.98 is rounded down), so the total is 2038. PayPal(500) has a fee of 45 and a total of 545, and Cash(700) has a total of 700. The three fees add up to 84.

    Approach: share what is common, leave the fee open

    Writing total() three times would repeat the same line. An abstract class avoids that. Payment keeps the amount and the one total() method, and declares that every payment has a fee() without saying what it is, because only a card, a PayPal payment or a cash payment knows its own fee.

    In Python, an abstract class inherits from ABC (from the abc module), and an abstract method is marked with @abstractmethod. While a class has an abstract method, Python refuses to create an object from it. A subclass becomes concrete by writing that method.

    The subclasses inherit __init__ and total and write only fee. When total() runs for a card, self.fee() finds Card.fee, so the shared method uses the right fee for each object.

    Step by step

    1. Store the amount. In Payment.__init__, set self.amount. It lives in the parent once and all three subclasses inherit it.
    2. Declare fee as abstract. Put @abstractmethod above it and use pass as its body. It says that every payment has a fee without writing one.
    3. Write total as self.amount + self.fee(), once, for all kinds of payment.
    4. Card's fee: self.amount * 2 // 100.
    5. PayPal's fee: self.amount * 3 // 100 + 30. The 30 cents come on top even for an amount of 0.
    6. Cash's fee: return 0. Cash must still define fee: a subclass that leaves it out stays abstract and can't be created.

    The solution in Python

    from abc import ABC, abstractmethod
    
    class Payment(ABC):
        def __init__(self, amount):
            self.amount = amount
    
        @abstractmethod
        def fee(self): pass
    
        def total(self):
            return self.amount + self.fee()
    
    class Card(Payment):
        def fee(self):
            return self.amount * 2 // 100
    
    class PayPal(Payment):
        def fee(self):
            return self.amount * 3 // 100 + 30
    
    class Cash(Payment):
        def fee(self):
            return 0
    

    Using the three payments from the first example:

    payments = [Card(1999), PayPal(500), Cash(700)]
    print(*[p.total() for p in payments])
    print(sum(p.fee() for p in payments))
    
    2038 545 700
    84
    

    The list comprehension calls the same total() on every item. Each object supplies its own fee through the shared method.

    Rounding: multiply first, then divide

    The operator // divides and rounds down, so 1999 * 2 // 100 is 39, while 1999 * 2 / 100 is the float 39.98. Order matters, too. Python evaluates amount * 2 // 100 left to right, multiplying first. If you divide first, the rounding happens too early: 1999 // 100 * 2 gives 38, not 39. And for an amount of 150, 150 // 100 * 2 is 2 while 150 * 2 // 100 is 3.

    What happens without the abstract method

    Trying to create the abstract class itself, or a subclass that has no fee, fails:

    class Free(Payment): pass
    
    Free(5)
    
    TypeError: Can't instantiate abstract class Free without an implementation for abstract method 'fee'
    

    The same error appears for Payment(5). That is the point of an abstract method: forgetting to write a fee is caught when the object is created, not in the middle of a calculation.

    Why the shared method calls self.fee()

    The line return self.amount + self.fee() is written in Payment, a class that has no idea what a card is. It still gives the right answer for a card, because self is the actual object, a Card, and Python looks for fee starting from the object's own class. This pattern, a fixed recipe in the parent with one step left to the subclasses, is the main reason to use an abstract class. If the rule for a total ever changes, say a minimum charge, you edit one method instead of three.

    The second example shows the rounding at small amounts. Card(49) has a fee of 49 * 2 // 100, which is 0, so its total is 49. PayPal(33) has 33 * 3 // 100, which is 0, plus 30, so its total is 63. Card(50) has a fee of exactly 1 and a total of 51. The fees add up to 31.

    Edge cases and common mistakes

    • Amount 0. Card(0) has total 0, Cash(0) has total 0, and PayPal(0) has total 30 because of the fixed 30 cents.
    • Small amounts. Card(49) has a fee of 0 (0.98 rounded down), so its total is 49. Card(50) has a fee of exactly 1.
    • Using / instead of //. The fee becomes a float such as 39.98 instead of whole cents.
    • Forgetting ABC. @abstractmethod is only enforced for classes that inherit from ABC. Without it, Payment could be created and fee() would quietly return None.
    • Writing total in each subclass. It works, but it repeats the formula. The whole idea is to write it once in the parent.

    Complexity

    Reading the amount and doing one multiplication, one division and one addition costs the same for a payment of 5 cents and one of a million.

    Every fee and every total is a few arithmetic operations, so each takes O(1) time.

    Keep going with Python500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free