Course contents

Encapsulation

Sign in to run this lab and save your progress

Learn

Before you start

Lesson 1 was about the mechanics — how to write a class and create objects. Lesson 2 is about the first big idea you'll use in every class you write: encapsulation.

The one-sentence version

Hide the data. Show only the actions.

Everything else in this lesson comes from that one sentence.

Why this matters in design interviews

Every design problem has rules that must always be true. A parking lot can't have negative spots. A Splitwise balance sheet must always add up to zero. A vending machine can't give you a snack it doesn't have.

Where do these rules live in your code?

  • Weak answer: "in the caller." Every place that touches the data has to remember the rules. This causes bugs.
  • Strong answer: "in the class." The class has methods that check the rules. Nobody outside the class can break them.

Encapsulation is how you do this. This lesson practices on a BankAccount because bank accounts have obvious rules (balance can't go negative, no free deposits, no overdrafts). Every design problem you'll meet later has less-obvious rules — but the same technique works.

Python's honor system

Python doesn't have private like Java does. A leading underscore (_balance) is just a convention that means "internal — don't touch". Nothing stops you.

Why work this way?

  • You can still test, mock, and inspect internals when you really need to. Helps with debugging.
  • The convention is everywhere. Every Python codebase you'll ever read treats _foo as internal.
  • Truly hidden data exists too (__balance with two underscores triggers something called "name mangling") but is almost never needed. Save it for advanced library authors.

About @property

@property turns a method into something that looks like an attribute when you read it:

a.balance      # calls balance(self) internally, returns the value
a.balance = 5  # AttributeError — no setter defined

You could write a get_balance() method instead. Python prefers the property style. It lets you start with a plain attribute and add validation later without breaking any caller. Writing Java-style getters everywhere is a code smell in Python.

What you're building

A BankAccount that:

  • Rejects impossible starting balances
  • Only accepts positive deposits and withdrawals
  • Refuses to let you take out more than you have
  • Lets callers READ the balance but not write to it

The test file has 9 tests, one per rule. If your code lets even one slip, some caller — hours or years from now — will hit exactly that case in production.

Your task

Lesson 1 showed you how to write a class. This lesson teaches the first big idea you'll use in every class: encapsulation.

Encapsulation is a two-word idea: hide the data, show only the actions.

Look at a BankAccount that just stores its balance in plain view:

class BankAccount:
    def __init__(self, initial):
        self.balance = initial

a = BankAccount(100)
a.balance = -5000     # nothing stops you

The class has no protection. Anyone can set the balance to anything — even a negative number that makes no sense. Bugs show up far from where they started.

Encapsulation fixes this. Make the balance private, and only let callers change it through methods that check the rules:

class BankAccount:
    def __init__(self, initial):
        self._balance = initial

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("deposit must be positive")
        self._balance += amount

    def withdraw(self, amount):
        if amount <= 0:
            raise ValueError("withdraw must be positive")
        if amount > self._balance:
            raise ValueError("insufficient funds")
        self._balance -= amount

    @property
    def balance(self):
        return self._balance

Three things to notice:

  1. _balance starts with an underscore. In Python, that means "this is internal — don't touch it from outside". Python doesn't force you to follow this rule, but every Python developer does.
  2. deposit and withdraw are how callers change the balance. They check the rules first (positive amounts, no overdrafts).
  3. @property lets callers READ a.balance like a normal attribute, but they can't write to it. No setter = no assignment allowed.

Why not just use a dict?

Beginners often ask: "why bother with a class? I could just use {'balance': 100}."

You could. But every place in your code that reads or writes that dict would need to check the rules itself. Miss one place → bug. A class puts the rules in one spot, behind the methods.

Your task

Build a BankAccount class in main.py with:

  1. __init__(self, initial) — stores balance privately. Raise ValueError if initial is negative.
  2. deposit(self, amount) — adds to balance. Raise ValueError if amount is zero or negative.
  3. withdraw(self, amount) — subtracts from balance. Raise ValueError if amount is zero or negative, or if you don't have enough money.
  4. balance — a read-only property that returns the current balance.

The tests check every rule, including that writing directly to .balance fails. Click Submit to run the tests.

Starter files

Preview only — open the lab to edit and run.

files_dir
02-encapsulation-starter