Welcome back, @name! Darwin here, ready to dive deeper into OOP.
In the previous lesson you learned the concept of classes and objects. Now it's time to learn how to create your own classes from scratch - just like biologists classify new species in the field!
A class consists of several key elements:
1class Animal: # 1. Class name (PascalCase)
2 """Class documentation - description of what it represents""" # 2. Docstring
3
4 species_count = 0 # 3. Class attribute (shared)
5
6 def __init__(self, name, age): # 4. Constructor
7 """Initializes a new object"""
8 self.name = name # 5. Instance attributes
9 self.age = age
10 Animal.species_count += 1 # Increment counter
11
12 def make_sound(self): # 6. Instance method
13 """Animal makes a sound"""
14 return f"{self.name} makes a sound!"
15
16 @classmethod # 7. Class method
17 def get_species_count(cls):
18 """Return the number of created animals"""
19 return cls.species_count
20
21 @staticmethod # 8. Static method
22 def is_adult(age):
23 """Check if adult (no access to self/cls)"""
24 return age >= 2
25
26# Creating objects
27lion = Animal("Simba", 3)
28elephant = Animal("Dumbo", 5)
29
30print(lion.name) # "Simba" - instance attribute
31print(Animal.get_species_count()) # 2 - class method
32print(Animal.is_adult(3)) # True - static methodclassSyntax:
class ClassName:Naming conventions:
Species, AnimalBehavior, DataProcessorLion ✅, not RunsLion ❌1# ✅ Good names
2class Species:
3 pass
4
5class ExpeditionTeam:
6 pass
7
8class GeneticAnalyzer:
9 pass
10
11# ❌ Bad names
12class species: # lowercase
13 pass
14
15class data: # too generic
16 pass
17
18class doSomething: # camelCase + verb
19 pass__init__The constructor (
__init__) is a special method called automatically when creating an object.1class Species:
2 def __init__(self, name, habitat):
3 """
4 Constructor - initializes a new species
5
6 Args:
7 name: Species name
8 habitat: Natural environment
9 """
10 print(f"Creating new species: {name}")
11 self.name = name # Instance attribute
12 self.habitat = habitat
13 self.discovered_date = "2024-01-01" # Default value
14
15# Calling the constructor
16lion = Species("Lion", "savanna")
17# Automatically calls: Species.__init__(lion, "Lion", "savanna")
18# Prints: "Creating new species: Lion"
19
20print(lion.name) # "Lion"
21print(lion.habitat) # "savanna"self parameter
is a reference to the current object. Python automatically passes the object as the first argument!self
1class Animal:
2 def __init__(self, name):
3 self.name = name # self = current object
4 # self.name means "the 'name' attribute of THIS object"
5
6 def greet(self):
7 # self allows access to the object's attributes
8 return f"Hi, I'm {self.name}!"
9
10lion = Animal("Simba")
11# Call: lion.greet()
12# Python automatically: Animal.greet(lion) ← lion becomes self!
13
14elephant = Animal("Dumbo")
15# elephant.greet() → Animal.greet(elephant) ← elephant is self!
16
17print(lion.greet()) # "Hi, I'm Simba!"
18print(elephant.greet()) # "Hi, I'm Dumbo!"NOTE: You can use a different name than
self, but it's a strong Python convention!1# ✅ Standard (use this!)
2def __init__(self, name):
3 self.name = name
4
5# 😕 Technically correct, but DON'T DO THIS
6def __init__(this, name): # Like in Java/C#
7 this.name = name
8
9def __init__(obj, name): # Confusing
10 obj.name = nameInstance attributes belong to a specific object. Each object has its own values!
1class Species:
2 def __init__(self, name, count):
3 self.name = name # Instance attribute
4 self.count = count # Each object has its own value
5
6lion = Species("Lion", 500)
7elephant = Species("Elephant", 300)
8
9print(lion.count) # 500 - separate value
10print(elephant.count) # 300 - separate value
11
12lion.count = 550 # Changes ONLY the lion
13print(lion.count) # 550
14print(elephant.count) # 300 - unchanged!Class attributes belong to the class and are shared by all objects!
1class Species:
2 total_species = 0 # Class attribute - shared!
3
4 def __init__(self, name):
5 self.name = name # Instance attribute
6 Species.total_species += 1 # Increment shared counter
7
8lion = Species("Lion")
9elephant = Species("Elephant")
10python = Species("Python")
11
12print(Species.total_species) # 3 - the class knows about all!
13print(lion.total_species) # 3 - can also access through instance
14print(elephant.total_species) # 3 - same value for allWhen to use which?
| Attribute type | When to use | Example | |----------------|-------------|---------| | Instance | Data unique to the object |
self.name, self.age |
| Class | Shared data | counters, constants, configuration |1class Species:
2 # Class attributes
3 kingdom = "Animalia" # Constant for all
4 species_count = 0 # Counter
5
6 def __init__(self, name, population):
7 # Instance attributes
8 self.name = name # Unique for each
9 self.population = population
10 Species.species_count += 1
11
12lion = Species("Lion", 500)
13elephant = Species("Elephant", 300)
14
15print(Species.kingdom) # "Animalia" - shared
16print(lion.name) # "Lion" - unique
17print(elephant.name) # "Elephant" - differentInstance methods operate on a specific object and have access to
self.1class Species:
2 def __init__(self, name, population):
3 self.name = name
4 self.population = population
5 self.observations = []
6
7 def add_observation(self, location, count):
8 """Add observation - instance method"""
9 self.observations.append({
10 "location": location,
11 "count": count
12 })
13 print(f"✓ Observation {self.name}: {count} in {location}")
14
15 def get_total_observed(self):
16 """Calculate total number of observations"""
17 return sum(obs["count"] for obs in self.observations)
18
19 def is_endangered(self):
20 """Check if endangered (< 100 individuals)"""
21 return self.population < 100
22
23# Usage
24lion = Species("Lion", 500)
25lion.add_observation("Serengeti", 25)
26lion.add_observation("Masai Mara", 18)
27
28print(f"Total observed: {lion.get_total_observed()}") # 43
29print(f"Endangered? {lion.is_endangered()}") # False@classmethodClass methods operate on the entire class, not on a specific object. They have access to
cls.1class Species:
2 all_species = [] # Registry of all species
3
4 def __init__(self, name):
5 self.name = name
6 Species.all_species.append(self)
7
8 @classmethod
9 def get_species_count(cls):
10 """Return the number of all species"""
11 return len(cls.all_species)
12
13 @classmethod
14 def find_by_name(cls, name):
15 """Find species by name - factory method"""
16 for species in cls.all_species:
17 if species.name == name:
18 return species
19 return None
20
21 @classmethod
22 def clear_registry(cls):
23 """Clear the registry"""
24 cls.all_species.clear()
25
26# Usage
27lion = Species("Lion")
28elephant = Species("Elephant")
29python = Species("Python")
30
31print(Species.get_species_count()) # 3 - class method
32
33found = Species.find_by_name("Elephant")
34print(found.name) # "Elephant"Difference
vs self
:cls
self = specific object (instance)cls = class (type)@staticmethodStatic methods have NO access to either
self or cls. They are regular functions in the class namespace.1class Species:
2 @staticmethod
3 def is_valid_name(name):
4 """Check if the species name is valid"""
5 # No self, no cls - only arguments!
6 return (
7 len(name) > 0 and
8 name[0].isupper() and
9 " " not in name
10 )
11
12 @staticmethod
13 def calculate_biodiversity_index(species_count, area_km2):
14 """Calculate biodiversity index"""
15 return species_count / area_km2 if area_km2 > 0 else 0
16
17# Usage - no object needed!
18print(Species.is_valid_name("Lion")) # True
19print(Species.is_valid_name("lion")) # False - lowercase
20print(Species.is_valid_name("")) # False - empty
21
22index = Species.calculate_biodiversity_index(150, 1000)
23print(f"Biodiversity index: {index}") # 0.15When to use static methods?
1class Species:
2 species_count = 0 # Class attribute
3
4 def __init__(self, name):
5 self.name = name
6 Species.species_count += 1
7
8 # 1. Instance method - has self
9 def describe(self):
10 """Access to self.name"""
11 return f"Species: {self.name}"
12
13 # 2. Class method - has cls
14 @classmethod
15 def get_count(cls):
16 """Access to cls.species_count"""
17 return cls.species_count
18
19 # 3. Static method - no self/cls
20 @staticmethod
21 def is_valid_name(name):
22 """Only arguments, no access to class/object"""
23 return len(name) > 0
24
25lion = Species("Lion")
26
27# 1. Instance method - needs an object
28print(lion.describe()) # OK - works on object
29# print(Species.describe()) # ERROR - no object!
30
31# 2. Class method - works on class
32print(Species.get_count()) # OK - works on class
33print(lion.get_count()) # OK - also works (Python passes the class)
34
35# 3. Static method - always works
36print(Species.is_valid_name("Lion")) # OK
37print(lion.is_valid_name("Lion")) # OK - also works1class SpeciesCatalog:
2 """
3 Complete Safari species cataloging system
4
5 Demonstrates all concepts: attributes, methods, constructor
6 """
7
8 # Class attribute - registry of all species
9 all_species = {} # {name: Species object}
10 next_id = 1
11
12 def __init__(self, scientific_name, common_name, habitat, diet, dangerous=False):
13 """
14 Constructor - creates a new species in the catalog
15
16 Args:
17 scientific_name: Scientific name (e.g., "Panthera leo")
18 common_name: Common name (e.g., "Lion")
19 habitat: Environment ("savanna", "jungle", "mountains", etc.)
20 diet: Diet ("carnivore", "herbivore", "omnivore")
21 dangerous: Whether dangerous to humans
22 """
23 # Instance attributes - unique for each species
24 self.id = SpeciesCatalog.next_id
25 SpeciesCatalog.next_id += 1
26
27 self.scientific_name = scientific_name
28 self.common_name = common_name
29 self.habitat = habitat
30 self.diet = diet
31 self.dangerous = dangerous
32
33 self.observations = [] # List of observations
34 self.population_estimate = 0
35 self.conservation_status = "Unknown" # Unknown by default
36
37 # Add to class registry
38 SpeciesCatalog.all_species[scientific_name] = self
39
40 # === INSTANCE METHODS (operate on a specific species) ===
41
42 def add_observation(self, date, location, count, notes=""):
43 """Add a field observation of the species"""
44 observation = {
45 "date": date,
46 "location": location,
47 "count": count,
48 "notes": notes
49 }
50 self.observations.append(observation)
51 self.population_estimate += count
52 return f"✓ Added observation: {count}x {self.common_name}"
53
54 def get_total_observed(self):
55 """Return the total number of observed individuals"""
56 return sum(obs["count"] for obs in self.observations)
57
58 def update_conservation_status(self):
59 """Update conservation status based on population"""
60 total = self.get_total_observed()
61
62 if total == 0:
63 self.conservation_status = "Not observed"
64 elif total < 50:
65 self.conservation_status = "Critically Endangered"
66 elif total < 250:
67 self.conservation_status = "Endangered"
68 elif total < 1000:
69 self.conservation_status = "Vulnerable"
70 else:
71 self.conservation_status = "Least Concern"
72
73 return self.conservation_status
74
75 def get_risk_level(self):
76 """Calculate risk level for the team (0-10)"""
77 if not self.dangerous:
78 return 0
79
80 # Base risk level for dangerous species
81 risk = 5
82
83 # Increase risk if carnivorous
84 if self.diet == "carnivore":
85 risk += 3
86
87 # Decrease if population is small (rarely encountered)
88 if self.get_total_observed() < 50:
89 risk -= 2
90
91 return max(0, min(10, risk)) # Clamp to 0-10
92
93 def generate_report(self):
94 """Generate a species report"""
95 self.update_conservation_status()
96
97 report = f"""
98┌─────────────────────────────────────────────────────────┐
99│ ID: {self.id:03d} - {self.common_name.upper()}
100├─────────────────────────────────────────────────────────┤
101│ Scientific name: {self.scientific_name}
102│ Habitat: {self.habitat}
103│ Diet: {self.diet}
104│ Dangerous: {'⚠️ YES' if self.dangerous else '✓ No'}
105│ Risk level: {self.get_risk_level()}/10
106├─────────────────────────────────────────────────────────┤
107│ OBSERVATIONS:
108│ - Total count: {self.get_total_observed()} individuals
109│ - Number of observations: {len(self.observations)}
110│ - Conservation status: {self.conservation_status}
111└─────────────────────────────────────────────────────────┘
112 """.strip()
113 return report
114
115 # === CLASS METHODS (operate on the entire catalog) ===
116
117 @classmethod
118 def get_total_species(cls):
119 """Return the number of all registered species"""
120 return len(cls.all_species)
121
122 @classmethod
123 def find_by_name(cls, scientific_name):
124 """Find species by scientific name"""
125 return cls.all_species.get(scientific_name)
126
127 @classmethod
128 def get_by_habitat(cls, habitat):
129 """Return all species from a given habitat"""
130 return [
131 species for species in cls.all_species.values()
132 if species.habitat == habitat
133 ]
134
135 @classmethod
136 def get_dangerous_species(cls):
137 """Return all dangerous species"""
138 return [
139 species for species in cls.all_species.values()
140 if species.dangerous
141 ]
142
143 @classmethod
144 def get_most_observed(cls, n=5):
145 """Return the n most observed species"""
146 sorted_species = sorted(
147 cls.all_species.values(),
148 key=lambda s: s.get_total_observed(),
149 reverse=True
150 )
151 return sorted_species[:n]
152
153 @classmethod
154 def clear_catalog(cls):
155 """Clear the entire catalog (use with caution!)"""
156 cls.all_species.clear()
157 cls.next_id = 1
158 return "✓ Catalog cleared"
159
160 # === STATIC METHODS (helper tools) ===
161
162 @staticmethod
163 def validate_scientific_name(name):
164 """Validate scientific name (format: "Genus species")"""
165 parts = name.split()
166 if len(parts) != 2:
167 return False
168
169 genus, species = parts
170 # Genus capitalized, species lowercase
171 return genus[0].isupper() and species[0].islower()
172
173 @staticmethod
174 def calculate_biodiversity_index(species_list):
175 """Calculate Simpson's diversity index"""
176 if not species_list:
177 return 0.0
178
179 total = sum(s.get_total_observed() for s in species_list)
180 if total == 0:
181 return 0.0
182
183 # Simpson's index: 1 - Σ(ni/N)²
184 sum_squares = sum(
185 (s.get_total_observed() / total) ** 2
186 for s in species_list
187 )
188 return 1 - sum_squares
189
190 @staticmethod
191 def format_scientific_name(genus, species):
192 """Format scientific name (italic-style for console)"""
193 return f"{genus.capitalize()} {species.lower()}"
194
195# === USAGE DEMONSTRATION ===
196
197print("=== CREATING SPECIES CATALOG ===\n")
198
199# Creating species (constructor)
200lion = SpeciesCatalog(
201 scientific_name="Panthera leo",
202 common_name="Lion",
203 habitat="savanna",
204 diet="carnivore",
205 dangerous=True
206)
207
208elephant = SpeciesCatalog(
209 scientific_name="Loxodonta africana",
210 common_name="African Elephant",
211 habitat="savanna",
212 diet="herbivore",
213 dangerous=False
214)
215
216python = SpeciesCatalog(
217 scientific_name="Python regius",
218 common_name="Royal Python",
219 habitat="jungle",
220 diet="carnivore",
221 dangerous=False
222)
223
224crocodile = SpeciesCatalog(
225 scientific_name="Crocodylus niloticus",
226 common_name="Nile Crocodile",
227 habitat="river",
228 diet="carnivore",
229 dangerous=True
230)
231
232# Adding observations (instance methods)
233lion.add_observation("2024-01-15", "Serengeti", 12, "Hunting pride")
234lion.add_observation("2024-01-20", "Masai Mara", 8, "Family with cubs")
235elephant.add_observation("2024-01-16", "Amboseli", 45, "Large herd")
236python.add_observation("2024-01-18", "Congo Jungle", 3)
237crocodile.add_observation("2024-01-19", "Nile", 25, "On the riverbank")
238
239# Reports for individual species
240print(lion.generate_report())
241print()
242print(elephant.generate_report())
243
244# Class methods - catalog-wide statistics
245print(f"\n=== CATALOG STATISTICS ===")
246print(f"Total species count: {SpeciesCatalog.get_total_species()}")
247
248print(f"\nSavanna species:")
249for species in SpeciesCatalog.get_by_habitat("savanna"):
250 print(f" - {species.common_name} ({species.get_total_observed()} individuals)")
251
252print(f"\nDangerous species:")
253for species in SpeciesCatalog.get_dangerous_species():
254 risk = species.get_risk_level()
255 print(f" ⚠️ {species.common_name} - Risk: {risk}/10")
256
257print(f"\nTop 3 most observed:")
258for i, species in enumerate(SpeciesCatalog.get_most_observed(3), 1):
259 print(f" {i}. {species.common_name}: {species.get_total_observed()} individuals")
260
261# Static methods - tools
262print(f"\n=== VALIDATION ===")
263names_to_test = ["Panthera leo", "panthera leo", "Lion", "Canis lupus familiaris"]
264for name in names_to_test:
265 valid = "✓" if SpeciesCatalog.validate_scientific_name(name) else "✗"
266 print(f"{valid} {name}")
267
268# Biodiversity index
269all_species_list = list(SpeciesCatalog.all_species.values())
270biodiversity = SpeciesCatalog.calculate_biodiversity_index(all_species_list)
271print(f"\nBiodiversity index: {biodiversity:.3f}")In this lesson you learned:
class__init__self to access attributesBefore moving on:
self and cls@classmethod vs @staticmethodKey concept:
self is "me" - the object refers to itself to access its own data and behaviors!In the next lesson Darwin will teach you inheritance - how to create class hierarchies like an evolutionary tree! 🌳🦁🐘