Summary of Today’s Python Classroom: Guard the State – Encapsulation, Properties and a Calculator Built to Grow (7 Nov 2026)

Wednesday we met visibility conventions. Today we put them to work properly, then started a project that will carry us through the next several sessions.

1. Why Encapsulation Exists

Encapsulation means guarding an object’s internal state, so outside code can’t put it into a state that should be impossible.

Without it:

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

b1 = BankAccount(5000)
b1.balance -= 10000        # nothing stops this
print(b1.balance)          # -5000: a balance that should never exist

Any code, anywhere, can change the balance to anything. The rules about what a balance may be live nowhere.

2. First Fix: Methods That Enforce Rules

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

    def withdraw(self, amount):
        if amount > self.__balance:
            raise ValueError("Insufficient funds")
        self.__balance -= amount

    def get_balance(self):
        return self.__balance
  • withdraw validates before it changes anything.
  • get_balance lets you read the value without being able to change it.

About __balance: Python has no access modifiers. The double underscore triggers name mangling, so the name becomes _BankAccount__balance and b1.__balance fails from outside. It discourages tampering; it doesn’t make tampering impossible. Wednesday’s post has the details.

3. The Pythonic Way: @property

Getter and setter methods work, but Python offers something cleaner. @property makes a method look like an attribute.

class BankAccount:
    def __init__(self, balance):
        self.balance = balance          # goes through the setter below

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

    @balance.setter
    def balance(self, value):
        if value < 0:
            raise ValueError("Balance cannot be negative")
        self._balance = value

b1 = BankAccount(5000)
print(b1.balance)       # reads like an attribute
b1.balance = -100       # ValueError: the rule is enforced

What’s going on:

  • @property turns balance() into something you read as b1.balance, without brackets.
  • @balance.setter runs whenever someone assigns to it. The setter must use the same name as the property.
  • Assigning in __init__ through the property means the validation also runs when the object is created. BankAccount(-500) fails immediately.

A design trade-off worth noticing: for a real bank account, you probably wouldn’t provide a public setter at all. A balance should change only through deposit and withdraw, which say why it changed and can record it. The setter here is a clear way to learn @property. In a real design, ask whether a value should be settable at all, or only readable.

How other languages do it:

LanguageApproach
JavaExplicit getBalance() and setBalance() methods
C#Built-in properties with get and set accessors, much like Python’s @property
Python@property and @name.setter

4. The Project: A CLI Calculator Built to Grow

A command-line calculator, built twice: first the novice way, then the professional way, so the effect of design principles is something you see rather than take on trust.

Requirements:

  • Basic calculations: addition, subtraction and so on.
  • Complex calculations: simple interest, EMI, the area of a circle.
  • Extensibility: adding a new operation must not require changing code that already works.

That last requirement has a name: the Open/Closed Principle, the O in SOLID. Code should be open to extension and closed to modification. This project is how we’ll learn it.

5. Designing Before Coding

What every operation has in common:

In plain EnglishExample
CapabilityWhat it can doEvery operation can calculate
ContentsWhat it needsA radius for a circle; principal, rate and time for interest

The relationships:

RelationshipStatementIn code
Is aAn Addition is a CalculationInheritance
Has aA Calculator has many CalculationsComposition

The same two relationships from last week, now driving a real design.

6. Two Tools We’ll Use Next

Abstract classes

  • A blueprint that says every Calculation must have a calculate method.
  • Built with abc.ABC and @abstractmethod.
  • You can’t create an object from a class that hasn’t filled in every abstract method. Python stops you when you try to instantiate it.

Protocols

  • A newer way to describe what an object must be able to do, without requiring it to inherit from anything.
  • Close to an interface in Java or .NET, with one key difference:
Java / .NET interfacePython Protocol
How a class qualifiesIt must declare it: implements, or : in C#Automatically, if it has the right methods
Name for thisNominal typingStructural typing
When it’s checkedBy the compilerBy a type checker such as mypy, not at runtime (unless you add @runtime_checkable)

A protocol is duck typing made explicit: “if it has a calculate method, it’s a calculation.”

7. Project Setup

The project starts with a folder and a README.md holding the goals and the low-level class diagram. Design is written down before code is written. That habit is part of what this project teaches.

✅ Action Items

  1. Break the open BankAccount by setting a negative balance, then fix it three ways: with methods, with __balance, and with @property.
  2. Write a setter that validates, and confirm it runs when you create the object as well as when you assign later.
  3. Sketch the calculator design yourself before next session: the base calculation, three concrete operations including EMI, and the Calculator that holds them. Label every relationship as is-a or has-a.
  4. Read about typing.Protocol, and be ready to explain how it differs from a Java interface.

⏭️ Next up: the calculator, novice version first.

By continuous learner

enthusiastic technology learner

Leave a comment