Encapsulation
Encapsulation is the practice of bundling data and methods that operate on that data within a single class, while restricting direct access to some components. Python uses naming conventions (underscores) rather than strict access modifiers to indicate the intended visibility of attributes and methods.
Python's Access Levels
| Prefix | Name | Meaning |
|---|---|---|
attr |
Public | Accessible from anywhere |
_attr |
Protected | Internal use, subclasses OK (convention) |
__attr |
Private | Name mangled to prevent accidental access |
Public Attributes
Public attributes have no underscore prefix and can be freely accessed and modified from anywhere. They're part of the class's public interface.
Click Run to execute your code
Start Public, Encapsulate Later
Python philosophy: start with public attributes. Add encapsulation (underscore) when you actually need validation or computed values. Don't over-engineer prematurely.
Protected Attributes (_single underscore)
A single underscore prefix signals "this is internal, use at your own risk." It's a convention that Python developers respect, though it's not enforced by the language.
Click Run to execute your code
It's a Convention, Not Enforcement
Python won't stop you from accessing _protected attributes. The underscore is a
signal to other developers: "this is an implementation detail that may change." Respect the
convention in your own code.
Private Attributes (__double underscore)
Double underscore prefix triggers "name mangling" - Python renames the attribute to include the class name, making accidental access harder. Use this to prevent subclass attribute conflicts.
Click Run to execute your code
Name Mangling: __attr becomes _ClassName__attr
Python doesn't truly hide private attributes. It renames __secret to
_MyClass__secret. This prevents accidental name clashes in inheritance, but
determined users can still access it. Privacy in Python is about intent, not enforcement.
Getter and Setter Methods
To control access to attributes, provide getter and setter methods. These allow validation, computed values, and read-only attributes.
Click Run to execute your code
Python Has a Better Way: @property
Instead of explicit get_x() and set_x() methods, Python provides
the @property decorator for cleaner syntax. You'll learn this in the
Properties lesson.
Common Mistakes
1. Confusing _ and __ purposes
# Wrong - using __ just for "privacy"
class User:
def __init__(self, name):
self.__name = name # Overkill! Use single underscore
# Better - single underscore for internal attributes
class User:
def __init__(self, name):
self._name = name # Convention: internal use
# Use __ only to prevent subclass conflicts
2. Accessing private via name mangling
# Bad practice - bypassing name mangling
class Secret:
def __init__(self):
self.__value = 42
s = Secret()
print(s._Secret__value) # Works but DON'T DO THIS!
# If you need to access it, the class should provide a method
# Or reconsider your design
3. Forgetting validation in setters
# Pointless getter/setter - no validation
class Person:
def __init__(self, age):
self._age = age
def get_age(self):
return self._age
def set_age(self, age):
self._age = age # No validation! Why have a setter?
# Better - setter should validate
class Person:
def set_age(self, age):
if age < 0 or age > 150:
raise ValueError("Invalid age")
self._age = age
4. Returning mutable internal data
# Wrong - exposing internal list
class ShoppingCart:
def __init__(self):
self._items = []
def get_items(self):
return self._items # Caller can modify!
cart = ShoppingCart()
items = cart.get_items()
items.append("hacked!") # Modifies internal state!
# Correct - return a copy
def get_items(self):
return self._items.copy() # Safe copy
5. Over-encapsulating simple data classes
# Over-engineered for simple data
class Point:
def __init__(self, x, y):
self._x = x
self._y = y
def get_x(self): return self._x
def get_y(self): return self._y
def set_x(self, x): self._x = x
def set_y(self, y): self._y = y
# Better - just use public attributes for simple data
class Point:
def __init__(self, x, y):
self.x = x # Simple, direct
self.y = y
Exercise: Secure User Account
Task: Create a User class with proper encapsulation for sensitive data.
Requirements:
- Public:
username- visible to everyone - Protected:
_email- internal use with getter - Private:
__password- never exposed directly - Methods:
verify_password(),change_password() __str__: Show username and masked email
Click Run to execute your code
Show Solution
class User:
def __init__(self, username, email, password):
self.username = username # Public
self._email = email # Protected
self.__password = password # Private
def get_email(self):
return self._email
def verify_password(self, password):
return password == self.__password
def change_password(self, old_password, new_password):
if self.verify_password(old_password):
self.__password = new_password
return True
return False
def __str__(self):
# Mask email: first char + *** + @domain
parts = self._email.split('@')
masked = parts[0][0] + '***@' + parts[1]
return f"User: {self.username}, Email: {masked}"
# Test
user = User("alice", "[email protected]", "secret123")
print(user.username) # alice
print(user.get_email()) # [email protected]
print(user.verify_password("secret123")) # True
print(user.verify_password("wrong")) # False
print(user.change_password("secret123", "newpass")) # True
print(user) # User: alice, Email: a***@example.com
Summary
- Public (no prefix): Accessible from anywhere, part of public API
- Protected (
_single): Convention for internal use, subclasses OK - Private (
__double): Name mangled to prevent subclass conflicts - Name mangling:
__attrbecomes_ClassName__attr - Getters/setters: Methods for controlled access and validation
- Python relies on conventions, not enforcement - "we're all consenting adults"
What's Next?
Now that you understand encapsulation, learn about Polymorphism - how different classes can share the same interface, enabling flexible and reusable code through duck typing and method overriding.
Enjoying these tutorials?