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!
Encapsulation is one of the 4 pillars of OOP, which involves:
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!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.
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) # 4Use public attributes when:
_single_underscoreConvention: 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:
help() by default__double_underscoreName 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:
__name to _ClassName__name1class 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"@propertyProperty is an elegant way to create getters and setters that look like attributes!
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!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!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 attribute1from 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")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!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!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 attribute1class 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 query1# ❌ 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 = value1# ❌ 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 protection1# ✅ 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_hashIn this lesson you learned:
__name)@property, getters and settersBefore moving on:
name, _name, and __nameSafari 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! ✨🐍