DSM
60 XP
25 minsIntermediate60 XP

Encapsulation & Properties

account.balance = -5000. Nothing stopped that line — and now every method that trusted the balance is quietly wrong. Attributes are honest about being public... which means anyone can break your object's rules with one assignment. Time to give objects a way to say 'not like that'.

What you'll learn

  • Mark internals with _underscore and respect others' marks
  • Explain __name mangling and its actual purpose
  • Write @property getters for computed/derived attributes
  • Add setters that validate and reject bad state
  • Evolve public attribute → property without breaking callers

What

Encapsulation: separating an object's public API from its internals. Python's tools are conventions (_single underscore = internal), name mangling (__double), and @property — which lets attribute syntax run code, so reads can compute and writes can validate.

Why

Objects exist to maintain invariants — a balance that matches its history, a temperature above absolute zero, an email that contains @. Encapsulation makes those rules enforceable at the boundary, and @property lets you add enforcement LATER without breaking a single caller — Python's answer to why it has no getter/setter boilerplate.

Where it's used

Validated domain models, computed attributes (df.shape is one!), caching, deprecating fields gracefully, and every library whose attributes 'just know' derived values.

Where this runs in production

pandasdf.shape is a property

It looks like stored data but computes from the internals on every read — and df.shape = (2, 2) raises, because there's no setter. You've been using @property since your first DataFrame.

PydanticValidation at assignment

FastAPI request models reject bad data the instant a field is set — industrial-strength setter validation, the exact pattern this lesson hand-builds.

RequestsInternals marked, API stable

The requests library's Session keeps _-prefixed internals it can refactor freely — a decade of internal changes, near-zero breakage, because the underscore contract held.

Theory

The core ideas, in plain language.

Python has no `private` keyword — encapsulation runs on convention. A leading underscore (self._connection) declares 'internal: use at your own risk'; linters warn on outside access, but nothing is enforced. This is deliberate: 'we're all consenting adults' — the language trusts you to respect the marker, and in exchange debugging and testing can always reach inside.
class Account:
    def __init__(self, owner, balance):
        self.owner = owner          # public API
        self._balance = balance     # internal: touch via methods
        self._history = []          # internal

    def deposit(self, amount):
        self._balance += amount
        self._history.append(amount)

    def balance_report(self):
        return f'{self.owner}: ${self._balance}'

The underscore splits the class into two zones: owner and the methods are the public contract; _balance and _history are implementation the class may restructure at will. Callers who type the underscore are announcing they accept the breakage risk.

Analogy: The restaurant kitchen door

A restaurant has a dining room (public API) and a kitchen (_internals). The 'Staff Only' sign on the kitchen door isn't a lock — you CAN barge in — but everyone understands: rearrange the kitchen and your dinner is your problem. @property is the waiter: you ask for 'the bill' as if it were a thing sitting on a shelf, but the waiter computes it fresh from the kitchen's records. And a property SETTER is the waiter checking your credit card before accepting it — same simple gesture from your side, validation on theirs.

Key Concept
@property: attribute syntax, method power

Decorate a no-argument method with @property and it's READ as an attribute: obj.total, no parentheses — but your code runs. Perfect for derived values (total from items, area from radius, shape from data) because they can never go stale: computed on every read, always consistent with the source state. To callers it's indistinguishable from plain data — that's the point.

class Order:
    def __init__(self):
        self._items = []   # (name, price)

    def add(self, name, price):
        self._items.append((name, price))

    @property
    def total(self):
        return round(sum([p for _, p in self._items]), 2)

o = Order()
o.add('mug', 14.0)
o.add('tee', 22.5)
print(o.total)      # attribute syntax — the method runs
# o.total = 99      # AttributeError: no setter — read-only!

total is always right because it's never stored — no 'forgot to update the cached total' bug can exist. And with no setter defined, assignment raises: read-only attributes for free.

Key Concept
Setters: validation at the boundary

@total.setter (named after the property) intercepts ASSIGNMENT: obj.celsius = -300 runs your checks and raises before bad state lands. The idiom pairs a property `celsius` with storage `_celsius`: the setter validates then writes the underscore name. Note __init__ should assign self.celsius = value (through the setter!) so construction is validated by the same gate. Result: invalid state is impossible to reach through the public name.

Watch out
Keep properties cheap and honest

Attribute syntax promises attribute COST — callers will read obj.total in loops and f-strings without a second thought. A property that hits a database, sleeps, or mutates state betrays that promise (and a property that changes state on READ is a command-query violation in disguise). Rule: properties compute quickly from in-memory state; anything slower or effectful stays an explicit method — fetch_total() warns the caller with its parentheses.

Visual Learning

See the concept, then explore it.

One name, two gated paths: reading and writing temp.celsius

With @property + setter, attribute syntax routes through your code both ways. Click each node.

Worked Examples

Watch it built up, one line at a time.

Very EasyPublic face, private kitchen

Mark which parts of a class are contract vs implementation.

Step 1 of 1

Callers use tick() and elapsed_label(); _ticks is the class's own business. Tomorrow it could store timestamps instead — no caller notices, because none (politely) touched the underscore.

Code
01class Timer:
02 def __init__(self):
03 self._ticks = 0 # internal counter
04 
05 def tick(self):
06 self._ticks += 1
07 
08 def elapsed_label(self):
09 return f'{self._ticks} ticks'
10 
11t = Timer()
12t.tick()
13t.tick()
14print(t.elapsed_label())
Output
2 ticks
Practice Coding

Your turn — write the code.

Your task

Build a Product with a validated price: __init__(name, price) assigns through the property; the price setter rejects negatives with ValueError('price cannot be negative'); storage lives in _price; add a read-only property display returning '<name>: $<price formatted to 2dp>'. Demo: create one, reprice it, attempt a negative price (catch and print the error), print display.

Expected output
price cannot be negative
mug: $12.50

Write your solution in the editor on the right, then hit Run.

Exercises

Prove it. Reach 80% to complete the lesson.

Mastery Gate0% / 80% required
Easy0/2 solved

What does a single leading underscore (self._cache) mean in Python?

What does @property change about a method?

Medium0/3 solved

A property `total` has a getter but NO setter. What does obj.total = 100 do?

ScenarioA teammate stores self.total in __init__ and updates it inside add_item(). QA finds carts where total ≠ sum of items — a code path mutated _items without updating total.

What's the structural fix?

Class Circle: __init__(r) assigns through a validated property (radius must be > 0, else ValueError('radius must be positive')); read-only property area = round(3.14159 * r * r, 2). Create Circle(2), print area; try Circle(-1) and print the caught error. Expected: 12.57 radius must be positive

Validated construction:__init__ assigns via the property; Circle(-1) raises
Computed area:area is a read-only property derived from _radius
Hard0/2 solved

What does self.__token (double underscore) actually do?

ScenarioA review comment: 'user.recommendations is a @property that runs a 2-second ML inference call. Three templates read it in loops; page loads now take 30s.'

What design rule was broken?

Complete 80% more exercises to unlock.
Interview Prep

How this shows up in real interviews.

Python has no private keyword. How does encapsulation actually work, and why is it designed that way?

Show model answer

Through convention plus one mechanical assist. A single leading underscore marks an attribute or method as internal — nothing enforces it, but linters flag outside access, `from module import *` skips such names, and reviewers treat violations as contract breaches. Double leading underscores add name mangling (rewritten to _ClassName__attr), whose real purpose is preventing accidental name collisions between a base class and its subclasses, not secrecy — the mangled name is plainly visible in vars(). The philosophy ('consenting adults'): hard privacy mostly obstructs debugging, testing, and emergency patches, while a clear marker achieves the actual goal — communicating what's stable API versus refactorable implementation. The enforcement that matters is at the WRITE boundary anyway, and @property provides that: computed reads and validating setters give Python real invariant protection exactly where invariants live.

Why does Python code avoid Java-style getX()/setX() methods, and what replaces them?

Show model answer

Because @property makes the boilerplate unnecessary WITHOUT giving up the option of behavior later. Java bakes getters/setters in from day one defensively — changing a public field to a method breaks every caller, so you pay the ceremony up front just in case. Python's property decorator decouples syntax from mechanism: obj.email is attribute syntax whether it's a plain attribute or a property running validation code, so you ship the plain attribute first and convert to a property — same name, zero caller changes — only when a real need arrives (validation, normalization, derivation, deprecation warnings). That migration path is the whole argument: uniform access means you never pay for flexibility you don't use. The caveat that comes with it: properties must stay cheap and side-effect-free, because attribute syntax tells callers 'this is data' — an expensive or mutating property betrays the syntax and belongs behind an explicit method call.

How do you design a class so invalid states are unrepresentable? Walk through the tools.

Show model answer

Layer the defenses at the boundary. First, funnel all mutation through a narrow surface: storage is underscore-internal, and state changes go through commands (record(), deposit()) or property setters — never raw attribute pokes. Second, validate in the setter and route construction through it: __init__ assigns self.celsius = value (the public name), so the same gate checks initial values and later updates — no separate constructor validation to drift out of sync. Third, make derived values computed properties rather than stored fields (total from _items, mean from _readings): computed state cannot desync from its source, eliminating the entire 'forgot to update the cache' bug family. Fourth, make read-only things read-only by omitting setters — df.shape-style. Finally, raise early with specific exceptions (ValueError with the offending value in the message) so bad data fails at the boundary, not three functions downstream — which is the bridge to the next module: error handling is how these gates communicate.

Common Mistakes to Avoid

1) Bypassing your own gates — writing self._price = x from other methods when the setter has the validation; go through the public name. 2) Storing derived values (self.total) that mutation paths must remember to update — compute them as properties. 3) Infinite recursion: the getter returning self.price instead of self._price calls itself forever. 4) Expensive/side-effecting properties — attribute syntax promises attribute cost. 5) __double underscores as 'stronger private' — it's clash protection for inheritance, not security. 6) Forgetting __init__ should assign through the property so construction validates too.

Ask the AI Tutor

Try these prompts in the AI Tutor panel: • 'Quiz me: plain attribute, property, or method — for these eight fields?' • 'Walk me through the getter-recursion bug and why _name breaks the loop.' • 'Show the attribute→property migration on a class I paste.' • 'Explain name mangling with a subclass collision example.' • 'Interview mode: ask me why Python skips getters/setters and grade my answer.'

Glossary

Encapsulation — separating public API from internals. _single underscore — 'internal' by convention; unenforced. __double underscore — name mangling to _ClassName__attr; clash protection. @property — decorator making a method readable as an attribute. Getter/setter — the property's read and (optional) write functions. @x.setter — decorator registering the validating write path. Read-only property — getter without setter; assignment raises. Derived value — computed from other state; wants a property, not storage. Invariant — a rule the object's state must always satisfy. Uniform access — same syntax for stored and computed attributes.

Recommended Resources

• Docs: the built-in property() reference — the decorator trio (getter/setter/deleter) in one page. • Read: PEP 8's 'Designing for Inheritance' section — the official word on underscores and when to use properties. • Practice: take your Inventory class from Classes & Objects and make stock read-only from outside, with a computed total_items property. • Next in DSM: your setters raise ValueError — but what happens to a raised error, and how do callers catch it? Special Methods completes the OOP module first: __repr__, __eq__, __len__ — making your objects feel native.

Recap

✓ _underscore marks internals — a contract, not a lock; respect it both ways. ✓ @property runs code behind attribute syntax — derived values computed fresh, never stale. ✓ No setter = read-only; a setter = validation at the boundary, construction included. ✓ Public name gates, underscore name stores — and getters must read the underscore or recurse. ✓ Properties stay cheap and pure; slow or effectful work is a visible method call. ✓ Plain attributes first; upgrade to properties later with zero caller changes. Next up: Special Methods. Your classes work — now make them feel NATIVE: __repr__ for honest printing, __eq__ for ==, __len__ for len(), and the dunder protocol behind every Python operator.

scratchpad — preview this lesson's challenge anytime
product.pyPython
Ready
Output

Run your code to see the output here.