Python Arena
Step 1 of 4

Private attributes

The code that creates objects of a class and uses them is called the class's client. A well-made class keeps its objects in a sensible state whatever the client does: a thermostat should never be set to 45 degrees, and a bank balance shouldn't change except by deposits and withdrawals. Keeping the attributes of an object acceptable at all times is called keeping its integrity.

A method can check a value before storing it. But as long as the attribute itself is in reach, the client can skip the method and change the attribute directly:

class Thermostat:
    def __init__(self):
        self.target = 20

    def set_target(self, degrees):
        if 5 <= degrees <= 30:
            self.target = degrees


heater = Thermostat()
heater.set_target(45)
print(heater.target)
heater.target = 45
print(heater.target)
Output
20
45

The method refused 45, and then the client set it anyway.

Two underscores make it private

An attribute whose name starts with two underscores, such as __target, is private: only the class's own methods can use it by that name. Code outside the class that tries to read it gets an error. To let the client see the value, the class offers a method that returns it:

class Thermostat:
    def __init__(self):
        self.__target = 20

    def set_target(self, degrees):
        if 5 <= degrees <= 30:
            self.__target = degrees

    def get_target(self):
        return self.__target


heater = Thermostat()
heater.set_target(22)
print(heater.get_target())
print(heater.__target)
Output
22
Python stops with
Traceback (most recent call last):
  File "main.py", line 16, in <module>
    print(heater.__target)
          ^^^^^^^^^^^^^^^
AttributeError: 'Thermostat' object has no attribute '__target'. Did you mean: 'get_target'?

Inside the class, self.__target works as usual. Outside it, the attribute can't be reached by that name (Python even suggests the public method to use instead), so the only way to change the target is set_target, which checks the value.

Hiding an object's attributes and offering methods to use them is called encapsulation. The client tells the object what to do, and the object's own methods decide how. That also leaves the class free to change: if Thermostat later kept its target in tenths of a degree, only its methods would need rewriting, and every program that calls set_target and get_target would go on working.

Private, not secret

One thing outside code can do without an error is assign to the private name. But that doesn't reach the private attribute:

class Thermostat:
    def __init__(self):
        self.__target = 20

    def get_target(self):
        return self.__target


heater = Thermostat()
heater.__target = 45
print(heater.__target)
print(heater.get_target())
print(heater._Thermostat__target)
Output
45
20
20

heater.__target = 45 gave the object a new, unrelated attribute that happens to be called __target, and the thermostat's real setting stayed at 20. The last line shows why: inside a class, Python quietly renames every private name, putting an underscore and the class's name in front. So the real attribute is _Thermostat__target, and only code inside the class uses it by its short name.

That renaming is all "private" means in Python. Anyone who writes out the long name can still reach the attribute, so the two underscores aren't a lock but a label: this belongs to the class, use its methods. Treat it that way, and never write a name like _Thermostat__target in your own programs.

The two underscores go only at the start of the name. A name with two underscores at both ends, like __init__ or __str__, is one of Python's special methods, and it isn't private.

Your task

Challenge 1 of 3

Make the counter's attribute private. Fill in the blanks so that the attribute is called __count, and the program prints:

3

Challenge 2

A travel card holds money for bus trips. Write the class TravelCard with two private attributes, __balance and __trips, both 0 when a card is created, and these methods:

  • load(self, amount) adds the amount to the balance, but only if it's more than 0.
  • travel(self, price) pays for a trip: if the balance is at least the price, it takes the price off the balance, adds one to the trips and returns True. Otherwise nothing changes and it returns False.
  • __str__(self) returns a string such as balance 7.5, trips 1.

For example, this code:

card = TravelCard()
card.load(10)
card.load(-50)
print(card.travel(2.5))
print(card.travel(9))
print(card)

prints:

True
False
balance 7.5, trips 1

Challenge 3

A shop serves customers in the order they arrive. Write the class Queue, which keeps the names of the waiting customers in a private list __names:

  • join(self, name) adds a name to the end of the queue.
  • serve(self) removes the first name from the queue and returns it. If the queue is empty, it returns None.
  • length(self) returns the number of people waiting.

For example, this code:

queue = Queue()
queue.join("Ada")
queue.join("Ben")
print(queue.serve())
print(queue.length())
print(queue.serve())
print(queue.serve())

prints:

Ada
1
Ben
None