We use cookies to enhance your experience on the site
CodeWorlds

Encapsulation - protecting habitats

Welcome back, @name! Darwin here with a key lesson about data security.

Imagine you're maintaining a catalog of endangered species. Data about the locations of rare animals is sensitive - you don't want poachers to have direct access! You need controlled access, validation, protection.

In OOP this mechanism is encapsulation - hiding implementation details and protecting data from unauthorized access!

What is encapsulation?

Encapsulation is one of the 4 pillars of OOP, which involves:

  1. Grouping data and methods in a single class
  2. Hiding implementation details
  3. Controlling access to data through public methods

Safari Analogy: Sensitive data about endangered species locations is protected - access only for authorized personnel, with every change logged!

1# WITHOUT encapsulation - dangerous!
2class Species:
3    def __init__(self, name, population):
4        self.name = name
5        self.population = population  # Public - anyone can change!
6
7lion = Species("Lion", 500)
8lion.population = -999999  # ❌ Nobody stops this error!
9
10# WITH encapsulation - safe!
11class ProtectedSpecies:
12    def __init__(self, name, population):
13        self.name = name
14        self.__population = population  # Private!
15
16    def set_population(self, value):
17        """Controlled access with validation"""
18        if value < 0:
19            raise ValueError("Population cannot be negative!")
20        self.__population = value
21
22    def get_population(self):
23        return self.__population
24
25lion = ProtectedSpecies("Lion", 500)
26# lion.__population = -999  # ❌ Doesn't work - protected!
27lion.set_population(550)  # ✅ Works - validated!

Access conventions in Python

Python uses naming conventions to indicate access levels:

| Convention | Meaning | Example | |------------|---------|---------| |

name
| Public - accessible from anywhere |
self.name
| |
_name
| Protected - only class and subclasses |
self._internal_id
| |
__name
| Private - only inside the class |
self.__password
|

NOTE: In Python these are conventions, not hard restrictions! Python trusts the programmer.

Public attributes

By default all attributes are public - accessible from anywhere:

1class Animal:
2    def __init__(self, name, age):
3        self.name = name  # Public
4        self.age = age    # Public
5
6lion = Animal("Simba", 3)
7
8# Full access
9print(lion.name)  # "Simba"
10lion.age = 4      # Can modify
11print(lion.age)   # 4

Use public attributes when:

  • ✅ Data is safe for direct modification
  • ✅ You don't need validation
  • ✅ It's part of the class's public API

Protected attributes -
_single_underscore

Convention: An attribute with one underscore (

_name
) means "protected" - use only inside the class and in subclasses.

1class Species:
2    def __init__(self, name):
3        self.name = name           # Public
4        self._internal_id = 12345  # Protected - convention!
5
6    def _calculate_risk(self):
7        """Protected method - convention"""
8        return self._internal_id * 2
9
10lion = Species("Lion")
11
12# Technically possible, but violates convention
13print(lion._internal_id)  # 12345 - works, but SHOULDN'T!
14
15# Correct usage
16class EndangeredSpecies(Species):
17    def get_risk(self):
18        # Subclass can use _internal_id
19        return self._calculate_risk()

_underscore convention:

  • ⚠️ Signal: "This is internal implementation, don't use directly"
  • 🔓 Technical: Still accessible, Python doesn't block it
  • 📚 Documentation: Doesn't appear in
    help()
    by default

Private attributes -
__double_underscore

Name Mangling: An attribute with two underscores (

__name
) is "private" - Python changes the name to make access harder!

1class SecretData:
2    def __init__(self, value):
3        self.__secret = value  # Private!
4
5    def get_secret(self):
6        """Controlled access"""
7        return self.__secret
8
9data = SecretData("Top Secret")
10
11# Direct access doesn't work
12# print(data.__secret)  # ❌ AttributeError!
13
14# Works through method
15print(data.get_secret())  # "Top Secret"
16
17# Name mangling - Python changed the name!
18print(data._SecretData__secret)  # "Top Secret" - possible, but DON'T DO THIS!

Name Mangling mechanism:

  • Python changes
    __name
    to
    _ClassName__name
  • This hinders access but doesn't prevent it
  • Protects against accidental overriding in subclasses
1class Parent:
2    def __init__(self):
3        self.__private = "Parent"
4
5class Child(Parent):
6    def __init__(self):
7        super().__init__()
8        self.__private = "Child"  # No collision!
9
10child = Child()
11print(child._Parent__private)  # "Parent"
12print(child._Child__private)   # "Child"

Properties -
@property

Property is an elegant way to create getters and setters that look like attributes!

Basic property (read-only)

1class Circle:
2    def __init__(self, radius):
3        self.radius = radius
4
5    @property
6    def area(self):
7        """Calculate area - automatically!"""
8        return 3.14159 * self.radius ** 2
9
10    @property
11    def diameter(self):
12        """Calculate diameter"""
13        return self.radius * 2
14
15circle = Circle(5)
16
17# Use like an attribute, but calls a method!
18print(circle.area)      # 78.53975 - looks like an attribute
19print(circle.diameter)  # 10
20
21circle.radius = 10
22print(circle.area)      # 314.159 - automatically recalculated!

Property with getter and setter

1class Temperature:
2    def __init__(self, celsius):
3        self._celsius = celsius  # Internal storage
4
5    @property
6    def celsius(self):
7        """Getter - read temperature"""
8        return self._celsius
9
10    @celsius.setter
11    def celsius(self, value):
12        """Setter - write with validation"""
13        if value < -273.15:
14            raise ValueError("Temperature below absolute zero!")
15        self._celsius = value
16
17    @property
18    def fahrenheit(self):
19        """Automatic conversion"""
20        return self._celsius * 9/5 + 32
21
22    @fahrenheit.setter
23    def fahrenheit(self, value):
24        """Set via Fahrenheit"""
25        self._celsius = (value - 32) * 5/9
26
27temp = Temperature(25)
28
29# Getter
30print(temp.celsius)     # 25
31
32# Setter with validation
33temp.celsius = 30       # OK
34print(temp.celsius)     # 30
35
36# temp.celsius = -300  # ValueError: Temperature below absolute zero!
37
38# Automatic conversion
39print(temp.fahrenheit)  # 86.0
40temp.fahrenheit = 32    # Set via Fahrenheit
41print(temp.celsius)     # 0.0 - automatic conversion!

Read-only property

1class Person:
2    def __init__(self, birth_year):
3        self._birth_year = birth_year
4
5    @property
6    def birth_year(self):
7        """Read-only - no setter"""
8        return self._birth_year
9
10    @property
11    def age(self):
12        """Dynamically computed"""
13        from datetime import datetime
14        return datetime.now().year - self._birth_year
15
16person = Person(1990)
17print(person.age)  # 35 (in 2025)
18# person.age = 40  # ❌ AttributeError: can't set attribute

Safari example - Endangered species management system

1from datetime import datetime
2from typing import List, Dict
3
4class EndangeredSpecies:
5    """
6    Class managing an endangered species with full encapsulation
7
8    Protects sensitive data about location, population, observations
9    """
10
11    # Class stores all instances
12    _all_species: Dict[str, 'EndangeredSpecies'] = {}
13    _next_id = 1
14
15    def __init__(self, scientific_name, common_name, conservation_status):
16        # Public data - safe to share
17        self.scientific_name = scientific_name
18        self.common_name = common_name
19
20        # Protected data - _underscore convention
21        self._id = EndangeredSpecies._next_id
22        EndangeredSpecies._next_id += 1
23        self._conservation_status = conservation_status
24        self._created_at = datetime.now()
25
26        # Private data - sensitive information
27        self.__population = 0
28        self.__secret_locations = []  # Locations - NOT for public!
29        self.__access_log = []        # Access log
30
31        # Register in catalog
32        EndangeredSpecies._all_species[scientific_name] = self
33
34    # === PROPERTY: Population (with validation) ===
35
36    @property
37    def population(self):
38        """Read population - logged"""
39        self.__log_access("population", "read")
40        return self.__population
41
42    @population.setter
43    def population(self, value):
44        """Write population - validation + logging"""
45        self.__log_access("population", "write", f"Change from {self.__population} to {value}")
46
47        if not isinstance(value, int):
48            raise TypeError("Population must be an integer")
49
50        if value < 0:
51            raise ValueError("Population cannot be negative")
52
53        self.__population = value
54        self.__update_conservation_status()
55
56    # === PROPERTY: Conservation Status (read-only) ===
57
58    @property
59    def conservation_status(self):
60        """Conservation status - read-only"""
61        return self._conservation_status
62
63    # === PROPERTY: Threat Level (dynamically computed) ===
64
65    @property
66    def threat_level(self):
67        """
68        Threat level (1-10) - computed based on population
69        Read-only - no setter
70        """
71        if self.__population == 0:
72            return 10  # Critically endangered
73        elif self.__population < 50:
74            return 9
75        elif self.__population < 250:
76            return 7
77        elif self.__population < 1000:
78            return 5
79        elif self.__population < 5000:
80            return 3
81        else:
82            return 1  # Safe
83
84    # === PRIVATE METHODS ===
85
86    def __log_access(self, field, action, details=""):
87        """Private method - access logging"""
88        log_entry = {
89            "timestamp": datetime.now(),
90            "field": field,
91            "action": action,
92            "details": details
93        }
94        self.__access_log.append(log_entry)
95
96    def __update_conservation_status(self):
97        """Private method - update status based on population"""
98        if self.__population == 0:
99            self._conservation_status = "Extinct in the Wild"
100        elif self.__population < 50:
101            self._conservation_status = "Critically Endangered"
102        elif self.__population < 250:
103            self._conservation_status = "Endangered"
104        elif self.__population < 1000:
105            self._conservation_status = "Vulnerable"
106        else:
107            self._conservation_status = "Least Concern"
108
109    # === PUBLIC METHODS - Controlled access ===
110
111    def add_secret_location(self, latitude, longitude, authorization_code):
112        """
113        Add a sensitive location - requires authorization!
114
115        Args:
116            latitude: Latitude
117            longitude: Longitude
118            authorization_code: Access code (in production: hash, token, etc.)
119        """
120        # Authorization validation
121        if authorization_code != "SAFARI_2024_SECURE":
122            self.__log_access("secret_locations", "denied", "Unauthorized access")
123            raise PermissionError("Unauthorized access to sensitive data!")
124
125        # Coordinate validation
126        if not (-90 <= latitude <= 90):
127            raise ValueError("Invalid latitude")
128        if not (-180 <= longitude <= 180):
129            raise ValueError("Invalid longitude")
130
131        location = {
132            "lat": latitude,
133            "lon": longitude,
134            "added_at": datetime.now()
135        }
136
137        self.__secret_locations.append(location)
138        self.__log_access("secret_locations", "add", f"Added location: {latitude}, {longitude}")
139
140        return f"✓ Location added (ID: {len(self.__secret_locations)})"
141
142    def get_location_count(self):
143        """Return number of locations without revealing details"""
144        return len(self.__secret_locations)
145
146    def get_general_area(self):
147        """Return general area without precise coordinates"""
148        if not self.__secret_locations:
149            return "No location data"
150
151        # Average coordinates for general area
152        avg_lat = sum(loc["lat"] for loc in self.__secret_locations) / len(self.__secret_locations)
153        avg_lon = sum(loc["lon"] for loc in self.__secret_locations) / len(self.__secret_locations)
154
155        # Return rounded to 1 decimal (less precise)
156        return f"Area: {avg_lat:.1f}°, {avg_lon:.1f}°"
157
158    def get_access_log(self, authorization_code):
159        """Return access log - requires authorization"""
160        if authorization_code != "SAFARI_2024_SECURE":
161            raise PermissionError("Unauthorized access to logs!")
162
163        return self.__access_log.copy()  # Return a copy, not the original!
164
165    def generate_public_report(self):
166        """Public report - without sensitive data"""
167        return f"""
168╔════════════════════════════════════════════════╗
169  {self.common_name} ({self.scientific_name})
170╠════════════════════════════════════════════════╣
171  Conservation status: {self.conservation_status}
172  Population: {self.population} individuals
173  Threat level: {self.threat_level}/10
174  Known locations: {self.get_location_count()}
175  General area: {self.get_general_area()}
176╚════════════════════════════════════════════════╝
177        """.strip()
178
179    def generate_classified_report(self, authorization_code):
180        """Detailed report - requires authorization"""
181        if authorization_code != "SAFARI_2024_SECURE":
182            raise PermissionError("Unauthorized access to classified data!")
183
184        locations_str = "\n".join(
185            f"    - Lat: {loc['lat']:.6f}, Lon: {loc['lon']:.6f} (added: {loc['added_at'].strftime('%Y-%m-%d %H:%M')})"
186            for loc in self.__secret_locations
187        )
188
189        return f"""
190╔════════════════════════════════════════════════╗
191  🔒 CLASSIFIED REPORT - {self.common_name}
192╠════════════════════════════════════════════════╣
193  ID: {self._id}
194  Status: {self.conservation_status}
195  Population: {self.__population}
196  Threat: {self.threat_level}/10
197
198  LOCATIONS (CLASSIFIED):
199{locations_str if locations_str else "    No data"}
200
201  Data accesses: {len(self.__access_log)}
202╚════════════════════════════════════════════════╝
203        """.strip()
204
205    # === CLASS METHODS ===
206
207    @classmethod
208    def get_species_by_threat_level(cls, min_level):
209        """Return species with threat level >= min_level"""
210        return [
211            species for species in cls._all_species.values()
212            if species.threat_level >= min_level
213        ]
214
215# === USAGE DEMONSTRATION ===
216
217print("=== ENDANGERED SPECIES MANAGEMENT SYSTEM ===\n")
218
219# Creating species
220rhino = EndangeredSpecies(
221    scientific_name="Diceros bicornis",
222    common_name="Black Rhinoceros",
223    conservation_status="Critically Endangered"
224)
225
226mountain_gorilla = EndangeredSpecies(
227    scientific_name="Gorilla beringei beringei",
228    common_name="Mountain Gorilla",
229    conservation_status="Endangered"
230)
231
232# Setting population (with validation)
233rhino.population = 45  # OK
234mountain_gorilla.population = 880  # OK
235
236# Attempting invalid value
237try:
238    rhino.population = -10  # ValueError!
239except ValueError as e:
240    print(f"❌ Validation error: {e}\n")
241
242# Adding location (requires authorization)
243try:
244    rhino.add_secret_location(
245        latitude=-1.2345,
246        longitude=36.7890,
247        authorization_code="WRONG_CODE"
248    )
249except PermissionError as e:
250    print(f"❌ No authorization: {e}\n")
251
252# Correct authorization
253rhino.add_secret_location(-1.2345, 36.7890, "SAFARI_2024_SECURE")
254rhino.add_secret_location(-1.5678, 36.9012, "SAFARI_2024_SECURE")
255mountain_gorilla.add_secret_location(-1.4321, 29.6543, "SAFARI_2024_SECURE")
256
257print("✓ Locations added\n")
258
259# Direct access to private data - doesn't work!
260try:
261    print(rhino.__population)  # AttributeError
262except AttributeError:
263    print("❌ Cannot directly access __population\n")
264
265# Access through property - works!
266print(f"✓ Rhino population (via property): {rhino.population}\n")
267
268# Public report - safe
269print("=== PUBLIC REPORT ===")
270print(rhino.generate_public_report())
271
272# Classified report - requires authorization
273print("\n=== CLASSIFIED REPORT ===")
274print(rhino.generate_classified_report("SAFARI_2024_SECURE"))
275
276# Read-only properties
277print(f"\n=== READ-ONLY PROPERTIES ===")
278print(f"Conservation status: {rhino.conservation_status}")
279print(f"Threat level: {rhino.threat_level}/10")
280
281# Attempt to set - doesn't work!
282try:
283    rhino.threat_level = 1  # AttributeError - no setter!
284except AttributeError as e:
285    print(f"❌ Cannot set threat_level: {e}")
286
287# Automatic status update when population changes
288print(f"\n=== AUTOMATIC UPDATE ===")
289print(f"Before: population={rhino.population}, status={rhino.conservation_status}")
290rhino.population = 280  # Increase population
291print(f"After: population={rhino.population}, status={rhino.conservation_status}")
292
293# Species with high threat
294print(f"\n=== CRITICALLY THREATENED SPECIES ===")
295critical = EndangeredSpecies.get_species_by_threat_level(7)
296for species in critical:
297    print(f"  ⚠️  {species.common_name}: Threat {species.threat_level}/10")

Advantages of encapsulation

1. Data validation

1class Temperature:
2    def __init__(self):
3        self.__celsius = 0
4
5    @property
6    def celsius(self):
7        return self.__celsius
8
9    @celsius.setter
10    def celsius(self, value):
11        if value < -273.15:
12            raise ValueError("Below absolute zero!")
13        self.__celsius = value
14
15temp = Temperature()
16# temp.celsius = -300  # ValueError - validation!

2. Access control

1class BankAccount:
2    def __init__(self, balance):
3        self.__balance = balance
4
5    def withdraw(self, amount):
6        """Controlled access"""
7        if amount > self.__balance:
8            raise ValueError("Insufficient funds")
9        self.__balance -= amount
10
11account = BankAccount(1000)
12# account.__balance = 999999  # Doesn't work!
13account.withdraw(500)  # Only through method!

3. Implementation flexibility

1class Circle:
2    def __init__(self, radius):
3        self.__radius = radius
4
5    @property
6    def area(self):
7        """User doesn't know this is computed!"""
8        return 3.14159 * self.__radius ** 2
9
10# You can change the implementation without changing the interface
11circle = Circle(5)
12print(circle.area)  # User uses it like an attribute

4. Hiding complexity

1class Database:
2    def __init__(self):
3        self.__connection = None
4        self.__cache = {}
5
6    def __connect(self):
7        """Private - user doesn't need to know the details"""
8        print("Connecting to database...")
9
10    def query(self, sql):
11        """Public interface - simple"""
12        if not self.__connection:
13            self.__connect()
14        # ... execute query

Encapsulation best practices

1. Use properties instead of getters/setters

1# ❌ Java-style - verbose
2class Person:
3    def __init__(self, name):
4        self.__name = name
5
6    def get_name(self):
7        return self.__name
8
9    def set_name(self, value):
10        self.__name = value
11
12# ✅ Pythonic way - elegant
13class Person:
14    def __init__(self, name):
15        self._name = name
16
17    @property
18    def name(self):
19        return self._name
20
21    @name.setter
22    def name(self, value):
23        self._name = value

2. Don't overuse private attributes

1# ❌ Too paranoid
2class Person:
3    def __init__(self, name):
4        self.__name = name  # Unnecessary __
5
6# ✅ Trust conventions
7class Person:
8    def __init__(self, name):
9        self.name = name  # Simple attribute, if you don't need protection

3. Use encapsulation for sensitive data

1# ✅ Protecting sensitive data
2class User:
3    def __init__(self, username, password):
4        self.username = username         # Public
5        self.__password_hash = hash(password)  # Private!
6
7    def check_password(self, password):
8        return hash(password) == self.__password_hash

Summary

In this lesson you learned:

  • ✅ What encapsulation is and why it's important
  • ✅ The difference between public, protected, and private attributes
  • ✅ The name mangling mechanism (
    __name
    )
  • ✅ Creating properties with
    @property
    , getters and setters
  • ✅ Data validation on write
  • ✅ Controlled access to sensitive information
  • ✅ Practical examples from the endangered species management system

Checkpoint

Before moving on:

  • [ ] You understand the difference between
    name
    ,
    _name
    , and
    __name
  • [ ] You can create a property with getter and setter
  • [ ] You know when to use encapsulation
  • [ ] You understand the name mangling mechanism
  • [ ] You can validate data in a setter

Safari Analogy: Encapsulation is a protection system for sensitive data about endangered species - not everyone should have access to the locations of rare animals! 🔒🦏

In the next lesson Darwin will teach you magic methods - hidden behaviors that make your classes work like Python's built-in types! ✨🐍

Go to CodeWorlds