MyCodeSchool
Încapsulare în Python
mycodeschool.ro

Encapsulation in Python — a clean, Java‑friendly path

In Java you rely on access modifiers (private, protected, public). Python favors convention + properties. This guide shows how to express the same intent cleanly in Python, with side‑by‑side examples and a production‑ready starter template.

🧩 Properties instead of getters/setters
🫙 _single vs __double underscore
📦 Module & package boundaries
🧪 Pytest + type hints (mypy)
⚙️ Project scaffolding (venv/Poetry)

TL;DR — idiomatic mapping from Java → Python

Java
  • private int x;
  • getX() / setX()
  • package as boundary
  • Lombok for boilerplate
Python
  • _x (non‑public by convention), __x (name‑mangled)
  • @property / @x.setter
  • Module / package & __all__
  • @dataclass (frozen for immutability)

1) Encapsulation basics in Python

Key difference: Python does not enforce access modifiers. Encapsulation is achieved by convention, name‑mangling, and properties. IDEs and linters respect these conventions.

Conventions

Java — classic encapsulation
public final class BankAccount {
  private BigDecimal balance = BigDecimal.ZERO;

  public BigDecimal getBalance() { return balance; }

  public void deposit(BigDecimal amount) {
    if (amount.compareTo(BigDecimal.ZERO) <= 0)
      throw new IllegalArgumentException("amount must be positive");
    balance = balance.add(amount);
  }
}
Python — idiomatic equivalent
from decimal import Decimal

class BankAccount:
    def __init__(self, balance: Decimal = Decimal("0")) -> None:
        self._balance = balance  # non-public

    @property
    def balance(self) -> Decimal:
        return self._balance

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

Name‑mangling (__double underscore)

Use double underscore sparingly when you want to avoid collisions in subclasses. It’s not about security; it’s to prevent accidental access/override.

class Base:
    def __init__(self) -> None:
        self.__token = "secret"  # becomes _Base__token

class Child(Base):
    def leak(self):
        # print(self.__token)  # AttributeError
        return dir(self)  # you'll see _Base__token

Validation with @property

from dataclasses import dataclass

@dataclass
class Temperature:
    _celsius: float

    @property
    def celsius(self) -> float:
        return self._celsius

    @celsius.setter
    def celsius(self, value: float) -> None:
        if value < -273.15:
            raise ValueError("below absolute zero")
        self._celsius = value
Rule of thumb: Start with _single_underscore for non‑public, add @property only when you need validation, caching, or compatibility.

2) Encapsulation at module & package level

In Python, modules (files) and packages (directories with __init__.py) are the primary boundaries. You can hide helpers and limit the public API.

# mypkg/mathutil.py
__all__ = ["mean"]  # exported API from this module

def _sum(xs: list[float]) -> float:  # non-public helper
    s = 0.0
    for x in xs: s += x
    return s

def mean(xs: list[float]) -> float:
    if not xs: raise ValueError("empty")
    return _sum(xs)/len(xs)
# consumer
from mypkg.mathutil import *
mean([1,2,3])        # OK
_sum([1,2,3])        # NameError (not exported)

3) Dataclasses, immutability & slots

@dataclass generates boilerplate (init, repr, eq). Prefer it over writing trivial classes.

from dataclasses import dataclass, replace

@dataclass(frozen=True, slots=True)
class Point:
    x: float
    y: float

p = Point(1, 2)
# p.x = 3  # FrozenInstanceError (immutable)
q = replace(p, x=3)  # new instance ("builder-like")
Use slots=True to make attributes fixed and memory‑efficient, and to prevent accidentally creating new attributes (a subtle encapsulation win).

4) Interfaces in Python: ABCs & Protocols

No interfaces keyword; use abc.ABC or structural typing via typing.Protocol.

from abc import ABC, abstractmethod

class Repository(ABC):
    @abstractmethod
    def save(self, obj) -> None: ...

class InMemoryRepo(Repository):
    def __init__(self): self._data = []
    def save(self, obj) -> None: self._data.append(obj)
from typing import Protocol

class SupportsSave(Protocol):
    def save(self, obj) -> None: ...

# Any object with .save(obj) matches the Protocol — duck typing

5) Common pitfalls coming from Java

6) Starter project — production‑ready scaffold

This template uses a virtual environment, pytest, ruff (linter), black (formatter) and mypy (type checking). It exposes a small domain with encapsulation.

Project structure

my_app/
├─ pyproject.toml
├─ README.md
├─ src/
│  └─ my_app/
│     ├─ __init__.py
│     ├─ domain.py          # encapsulated entities, properties
│     └─ cli.py             # entry point (main)
└─ tests/
   └─ test_domain.py

pyproject.toml (PEP 621)

[project]
name = "my-app"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = []

[project.optional-dependencies]
dev = ["pytest", "mypy", "black", "ruff"]

[project.scripts]
my-app = "my_app.cli:main"

[tool.ruff]
line-length = 100

[tool.mypy]
python_version = "3.11"
strict = true

[tool.black]
line-length = 100

src/my_app/domain.py

from __future__ import annotations
from dataclasses import dataclass

@dataclass(slots=True)
class _Balance:  # non-public value object
    amount: int

    def deposit(self, cents: int) -> "_Balance":
        if cents <= 0:
            raise ValueError("amount must be positive")
        return _Balance(self.amount + cents)

class Account:
    def __init__(self, cents: int = 0) -> None:
        self._balance = _Balance(cents)

    @property
    def cents(self) -> int:
        return self._balance.amount

    def deposit(self, cents: int) -> None:
        self._balance = self._balance.deposit(cents)

src/my_app/cli.py

from __future__ import annotations
from my_app.domain import Account

def main() -> None:
    acc = Account(100)
    acc.deposit(50)
    print(f"Balance: {acc.cents} cents")

if __name__ == "__main__":
    main()

tests/test_domain.py

from my_app.domain import Account

def test_deposit_increases_balance():
    acc = Account(100)
    acc.deposit(25)
    assert acc.cents == 125

Setup & run

Windows (PowerShell)
py -3.11 -m venv .venv
.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
pip install -e .[dev]
pytest -q
python -m my_app  # or: my-app
macOS/Linux
python3.11 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
pip install -e .[dev]
pytest -q
python -m my_app  # or: my-app
Why editable install (-e .)? It lets you import your package from src/ during development without re-installing after each change.

7) Java → Python: quick encapsulation cheat‑sheet

Access
Java private → Python _attr + @property.
Immutability
Java records → Python @dataclass(frozen=True).
Packages
Java packages → Python modules/packages + __all__.
Getters
Java getX() → Python @property def x(self).
Setters
Java setX() → Python @x.setter with validation.
Builders
Lombok builder → dataclasses.replace() or attrs.evolve().

8) Tooling essentials

# .pre-commit-config.yaml
repos:
- repo: https://github.com/psf/black
  rev: 24.4.2
  hooks: [{ id: black }]
- repo: https://github.com/astral-sh/ruff-pre-commit
  rev: v0.5.5
  hooks: [{ id: ruff }, { id: ruff-format }]
- repo: https://github.com/pre-commit/mirrors-mypy
  rev: v1.10.0
  hooks: [{ id: mypy }]

9) FAQ

Q: Can I truly hide fields in Python?
A: You can discourage access (_x) or mangle names (__x), but nothing beats tests + conventions. For hard boundaries, hide behind module APIs.

Q: Should I ever use explicit getX() methods?
A: Only if you need an API compatible with Java callers or you’re in a codebase that forbids properties. Otherwise use @property.

Q: What about performance?
A: Properties are fast enough for most apps. Use slots=True for hot paths and avoid per‑instance __dict__ bloat.