ML objects carry sensitive internal state: raw file paths, normalization statistics, cached tensors, random seeds, and half-initialized weights. If every caller can reach in and mutate those directly, reproducibility breaks and bugs hide.
Encapsulation bundles data with the methods that are allowed to change it—and hides the rest. You expose a clean get_batch() or predict() while keeping preprocessing, locking, and validation inside the object.
Learning Objectives
By the end of this lesson, students should be able to:
- Explain encapsulation as hiding internal state and exposing a controlled public interface.
- Use naming conventions (
_singleand__doubleleading underscores) to signal visibility in Python. - Implement
@propertygetters and setters for validated attribute access. - Protect dataset invariants (label ranges, feature shapes) inside class methods.
- Recognize encapsulation in framework design (e.g.,
nn.Moduleparameter registration). - Avoid leaking mutable internal references that let callers corrupt object state.
Hiding Implementation Details
Encapsulation restricts direct access to an object’s internal representation. External code interacts through a public interface (methods and properties); internal attributes and helper logic stay private by convention or design.
Python does not enforce true privacy. Instead, conventions and properties provide discipline:
| Convention | Meaning | ML Example |
|---|---|---|
public_attr |
Part of the intended API | dataset.transform |
_protected |
Internal; subclasses may use | self._cache of preloaded images |
__private |
Name-mangled; harder to access accidentally | self.__raw_paths not meant for callers |
ML Example: Encapsulated Dataset
class NormalizedTabularDataset:
def __init__(self, features, labels):
self._validate(features, labels)
self.__features = features # internal storage
self.__labels = labels
self._mean, self._std = self._compute_stats(features)
def _validate(self, features, labels):
if len(features) != len(labels):
raise ValueError("Length mismatch")
if not features:
raise ValueError("Empty dataset")
def _compute_stats(self, features):
import numpy as np
arr = np.array(features, dtype=float)
return arr.mean(axis=0), arr.std(axis=0) + 1e-8
def __getitem__(self, index):
import numpy as np
x = np.array(self.__features[index], dtype=float)
x = (x - self._mean) / self._std # normalization hidden inside
return x, self.__labels[index]
def __len__(self):
return len(self.__labels)
@property
def num_samples(self):
return len(self)
Callers use dataset[i] and receive normalized features. They cannot accidentally skip normalization by reading raw __features without knowing the mangled name—and they should not need to.
Properties for Validated Access
Properties let you expose read-only or validated attributes:
class TrainingRun:
def __init__(self, learning_rate):
self.learning_rate = learning_rate # uses setter below
@property
def learning_rate(self):
return self._learning_rate
@learning_rate.setter
def learning_rate(self, value):
if value <= 0 or value > 1:
raise ValueError("learning_rate must be in (0, 1]")
self._learning_rate = float(value)
Invalid hyperparameters fail at assignment time—not three epochs into a silent divergence.
Don’t Leak Mutable Internals
class BadDataset:
def __init__(self, labels):
self._labels = labels
def get_labels(self):
return self._labels # returns reference — caller can mutate!
ds = BadDataset([0, 1, 0])
labels = ds.get_labels()
labels[0] = 99 # corrupts dataset internal state
Fix: Return a copy (return self._labels.copy()) or expose read-only views. Better: don’t expose internals; provide methods like __getitem__.
Encapsulation in PyTorch Modules
nn.Module encapsulates parameters. You call model.parameters() and model.state_dict()—not model._parameters directly in application code. The framework controls registration, device movement, and gradient hooks internally.
Public API
forward, train, eval, parameters, to(device)
Hidden Internals
Hook registration, parameter dict layout, backward graph wiring
Lesson
Good ML libraries expose minimal, stable surfaces and keep complexity private.
Config Objects vs. Global State
Encapsulating experiment configuration in a class beats scattered globals:
class ExperimentConfig:
def __init__(self, seed, lr, batch_size):
self._seed = seed
self._lr = lr
self._batch_size = batch_size
self._locked = False
def lock(self):
self._locked = True
def set_lr(self, value):
if self._locked:
raise RuntimeError("Config locked after training started")
self._lr = value
Once training starts, lock() prevents accidental mid-run hyperparameter changes that break experiment comparability.
Common Misconceptions
Reality: Convention, properties, and careful API design provide effective encapsulation. Framework users rarely touch private attributes—and shouldn’t.
Reality: Use properties when validation or computed values matter. Simple data containers (dataclasses) can expose fields directly when invariants are weak.
Quick Knowledge Check
- Short Answer: Why return a copy of internal lists from getters? Answer: Prevents callers from mutating encapsulated state.
- True/False: Double underscore attributes (
__x) are impossible to access in Python. Answer: False—name mangling makes it harder, not impossible. - Short Answer: How does a property setter help ML configs? Answer: Validates values (e.g., positive learning rate) at assignment time.
- Short Answer: What does encapsulation hide and what does it expose? Answer: It hides internal state and exposes a controlled public interface.
- True/False: A single leading underscore (
_cache) means “internal; subclasses may use.” Answer: True. - Multiple Choice: PyTorch encapsulation of parameters is typically via: (a) global lists, (b)
parameters()/state_dict(), (c) printing weights, (d) CSV export. Answer: (b). - Short Answer: Why lock a config after training starts? Answer: Prevent accidental mid-run hyperparameter changes that break experiment comparability.
- True/False: Every field should always have a getter and setter. Answer: False—use properties when validation or computed values matter.
- Short Answer: What should a dataset class enforce internally? Answer: Invariants such as label ranges, feature shapes, and matching lengths.
- Multiple Choice: Returning an internal list from a getter without copying: (a) is safest, (b) can leak mutable state, (c) encrypts data, (d) freezes tensors. Answer: (b).
Key Takeaways
- Encapsulation hides internal state; callers use a controlled public interface.
- Python uses naming conventions and
@propertyfor access control. - Datasets and models should enforce invariants internally (shapes, normalization, valid labels).
- Never leak mutable internal references—return copies or expose behavior via methods.
- Frameworks like PyTorch encapsulate parameter management behind
parameters()andstate_dict().
Hands-on: Give students a dataset class that returns self.__labels directly. Have them mutate the list from outside, then fix it with a copy or a property. Discuss when dataclasses can expose fields versus when validation setters are required.
Bridge: Point to PyTorch state_dict() as production encapsulation—callers save and load weights without touching private buffers.