Skip to content

Encapsulation and Access Modifiers#

Encapsulation bundles data with the methods that operate on it and controls how the outside world interacts with internal state. Python takes a different path from Java or C++: there are no true access modifiers — only conventions, name mangling, descriptors, and @property. Understanding this philosophy is essential for writing idiomatic Python and answering interview questions honestly.

This page expands the encapsulation sections in Introdicion to OOPs and Classes and OOP Basics.

At a glance
Track Python Advanced → Object Oriented Programming
Sections 13 major topics
Outline Use the right-hand TOC to jump

Topics: Python's trust-based model · The three naming conventions · When to use each level · @property — the Pythonic access gate · Read-only and write-only patterns · Descriptors — how @property enforces access · __slots__ — restricting attributes · Module-level encapsulation — the underscore prefix · … (+5 more)

  1. Python's trust-based model
  2. The three naming conventions
  3. When to use each level
  4. @property — the Pythonic access gate
  5. Read-only and write-only patterns
  6. Descriptors — how @property enforces access
  7. __slots__ — restricting attributes
  8. Module-level encapsulation — the underscore prefix
  9. @dataclass and encapsulation
  10. Encapsulation via composition
  11. __getattr__, __setattr__, __getattribute__ — dynamic access
  12. Comparison with other languages (interview talking points) … and 1 more sections

Python's trust-based model#

Language Mechanism Enforced?
Java private, protected, public Compile-time
C++ access specifiers Compile-time
Python _single, __double underscore Convention only (mostly)

Python's philosophy (from The Zen of Python): "We are all consenting adults here." Visibility is a social contract, not a compiler barrier — with one partial exception (name mangling).


The three naming conventions#

class Account:
    def __init__(self, owner: str, balance: float):
        self.owner = owner           # public — part of API
        self._balance = balance      # internal — "please don't touch"
        self.__pin = "1234"          # mangled — avoids subclass collisions

    def deposit(self, amount: float) -> None:
        self._validate(amount)
        self._balance += amount

    def _validate(self, amount: float) -> None:
        if amount <= 0:
            raise ValueError("amount must be positive")
Syntax Meaning Accessible as Use when
name Public API obj.name Stable interface
_name Internal / protected by convention obj._name still works Implementation details
__name Name mangled to _ClassName__name Harder accidental access Avoid subclass attribute clashes

Name mangling in action#

class Parent:
    def __init__(self):
        self.__secret = 42

class Child(Parent):
    def reveal(self):
        # self.__secret would look for _Child__secret — doesn't exist
        return self._Parent__secret   # 42 — still accessible!

p = Parent()
p._Parent__secret   # 42 — mangling is not encryption

Interview trap — __private is not security

Name mangling prevents accidental shadowing in subclasses, not intentional access. Never store secrets assuming __ hides them.


When to use each level#

Scenario Choice
Field is part of stable public API self.name
Internal helper method self._helper()
Subclass might define same name self.__internal
Validation on read/write @property over _field
Truly hide from subclasses Rare — reconsider design

Interview default: public attributes unless validation or invariants demand @property.


@property — the Pythonic access gate#

class Celsius:
    def __init__(self, temp: float):
        self.temp = temp   # routes through setter

    @property
    def temp(self) -> float:
        return self._temp

    @temp.setter
    def temp(self, value: float) -> None:
        if value < -273.15:
            raise ValueError("below absolute zero")
        self._temp = value

Properties provide controlled access without changing call syntax:

t = Celsius(25)
t.temp = 30      # uses setter — same syntax as public attr

Full details: @property Decorators.


Read-only and write-only patterns#

Read-only via property (no setter)#

class Circle:
    def __init__(self, radius: float):
        self.radius = radius

    @property
    def radius(self) -> float:
        return self._radius

    @radius.setter
    def radius(self, value: float) -> None:
        if value <= 0:
            raise ValueError("radius must be positive")
        self._radius = value

    @property
    def area(self) -> float:
        return 3.14159 * self._radius ** 2

Returning defensive copies#

class Team:
    def __init__(self):
        self._members: list[str] = []

    @property
    def members(self) -> list[str]:
        return self._members.copy()   # caller can't mutate internal list

    def add(self, name: str) -> None:
        self._members.append(name)

Without .copy(), team.members.append("intruder") would bypass encapsulation if you returned the internal list directly.


Descriptors — how @property enforces access#

Descriptors are objects with __get__, __set__, or __delete__:

class PositiveNumber:
    def __set_name__(self, owner, name):
        self.name = f"_{name}"

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return getattr(obj, self.name)

    def __set__(self, obj, value):
        if value <= 0:
            raise ValueError("must be positive")
        setattr(obj, self.name, value)

class Product:
    price = PositiveNumber()

    def __init__(self, price: float):
        self.price = price

Product(9.99).price = -1   # ValueError

This is advanced — know it exists; use @property in interviews unless asked.


__slots__ — restricting attributes#

class Point:
    __slots__ = ("x", "y")

    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y

p = Point(1, 2)
# p.z = 3   # AttributeError — no __dict__ for arbitrary attrs
Benefit Trade-off
Lower memory per instance Fixed attribute set
Slightly faster access Subclass __slots__ must include parent's slots
Prevents accidental new attrs Can't add dynamic attributes

Module-level encapsulation — the underscore prefix#

# mymodule.py
_public_api = "use me"
_internal_helper = "don't import me"

def public_function(): ...
def _private_helper(): ...
Import style Gets
from mymodule import * Only names not starting with _ (unless __all__ defined)
import mymodule Everything — convention still applies

Define __all__ for explicit public API:

__all__ = ["public_function", "PublicClass"]

@dataclass and encapsulation#

from dataclasses import dataclass, field

@dataclass
class Config:
    host: str
    port: int = 8080
    _token: str = field(default="", repr=False)   # hidden from repr
Option Effect
repr=False Exclude from __repr__
init=False Not in generated __init__
frozen=True Immutable — no reassignment

For sensitive fields, repr=False helps but doesn't prevent access — still convention-based.


Encapsulation via composition#

Often better than access modifiers — hide implementation behind an interface:

class Engine:
    def __init__(self):
        self._rpm = 0

    def accelerate(self) -> None:
        self._rpm += 100

class Car:
    def __init__(self):
        self._engine = Engine()   # has-a — engine internals hidden

    def drive(self) -> None:
        self._engine.accelerate()

See Inheritance and Multiple Inheritance for composition vs inheritance.


__getattr__, __setattr__, __getattribute__ — dynamic access#

class DynamicConfig:
    def __init__(self, **kwargs):
        super().__setattr__("_data", kwargs)

    def __getattr__(self, name: str):
        try:
            return self._data[name]
        except KeyError:
            raise AttributeError(name) from None

    def __setattr__(self, name: str, value):
        if name == "_data":
            super().__setattr__(name, value)
        else:
            self._data[name] = value

Use super().__setattr__ inside __setattr__ to avoid infinite recursion.


Comparison with other languages (interview talking points)#

Question Python answer
"Is _x private?" No — convention; still accessible
"Can I make truly private attrs?" No — use OS permissions / encryption for secrets
"Why no modifiers?" Reduces boilerplate; trust + tests + code review
"How do teams enforce?" Linters (_ prefix checks), type checkers, API docs
"When use @property?" Validation, computed fields, API stability

Practical patterns#

Lazy initialization of expensive resource#

class DataLoader:
    def __init__(self, path: str):
        self._path = path
        self._cache: list | None = None

    @property
    def data(self) -> list:
        if self._cache is None:
            self._cache = self._load()
        return self._cache

    def _load(self) -> list:
        with open(self._path) as f:
            return f.readlines()

Validated setter with public getter semantics#

class Percentage:
    def __init__(self, value: float):
        self.value = value

    @property
    def value(self) -> float:
        return self._value

    @value.setter
    def value(self, v: float) -> None:
        if not 0 <= v <= 100:
            raise ValueError("percentage must be 0-100")
        self._value = v

Interview traps (quick reference)#

Trap What goes wrong Safe approach
Assuming __x is secure Data still readable via mangled name Never store secrets in plain attrs
Returning internal mutable External code mutates state Return copy or immutable view
@property on everything Verbose, un-Pythonic Public attrs when no invariant
_ prefix on "private" methods in tests Tests break on refactor Test public behavior
__getattribute__ override Infinite recursion Delegate to super() carefully
Ignoring __slots__ inheritance Subclass attrs silently break Include parent slots in child

Mental model checklist#

  1. What is the practical difference between _name and __name?
  2. Why does Python avoid true private fields?
  3. When should you use @property instead of a public attribute?
  4. How does name mangling help with subclass attribute collisions?
  5. Why return .copy() from a property that exposes a list?

What's next#

Topic Page
Property deep dive @property Decorators
Attribute hooks Dunder or Magic Methods
Composition patterns Inheritance and Multiple Inheritance
Intermediate OOP Classes and OOP Basics