We use cookies to enhance your experience on the site
CodeWorlds

Creating your own species

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!

Anatomy of a class in Python

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 method

1. Defining a class -
class

Syntax:

class ClassName:

Naming conventions:

  • PascalCase (each word capitalized):
    Species
    ,
    AnimalBehavior
    ,
    DataProcessor
  • Descriptive name: what the class represents
  • Noun, not verb:
    Lion
    ✅, 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

2. Constructor -
__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"

The
self
parameter

self
is a reference to the current object. Python automatically passes the object as the first argument!

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 = name

3. Instance attributes vs class attributes

Instance attributes

Instance 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

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 all

When 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" - different

4. Instance methods

Instance 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

5. Class methods -
@classmethod

Class 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

self
vs
cls
:

  • self
    = specific object (instance)
  • cls
    = class (type)

6. Static methods -
@staticmethod

Static 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.15

When to use static methods?

  • ✅ Helper (utility) functions thematically related to the class
  • ✅ Validators, converters, calculators
  • ✅ Don't need access to object or class attributes

Comparison of method types

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 works

Safari example - Species cataloging system

1class 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}")

Summary

In this lesson you learned:

  • ✅ How to define classes with
    class
  • ✅ Creating a constructor with
    __init__
  • ✅ Using
    self
    to access attributes
  • ✅ The difference between instance and class attributes
  • ✅ Creating instance, class, and static methods
  • ✅ When to use each type of method
  • ✅ Building a complete cataloging system

Checkpoint

Before moving on:

  • [ ] You can define a class with a constructor
  • [ ] You understand the difference between
    self
    and
    cls
  • [ ] You know the difference between instance and class attributes
  • [ ] You know when to use
    @classmethod
    vs
    @staticmethod
  • [ ] You can create an object and call its methods

Key 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! 🌳🦁🐘

Go to CodeWorlds