Welcome back, @name! Darwin here with a lesson about precision and code documentation.
In biology, precise classification is key - "big animal" is not the same as "Panthera leo (African lion), male, adult, 190kg". In Python, type hints (type annotations) allow the same precision in code!
1# Imprecise - what is this?
2def count_animals(animals):
3 return len(animals)
4
5# Precise - everything is clear!
6def count_animals(animals: list[str]) -> int:
7 """Count animals in the list"""
8 return len(animals)Type hints are type annotations in Python - additional information about the types of variables, function parameters, and return values.
IMPORTANT: Type hints are optional and do NOT affect program execution! Python ignores them at runtime. They serve for:
1# Without type hints - we have to guess
2def calculate_density(count, area):
3 return count / area
4
5# With type hints - everything is clear!
6def calculate_density(count: int, area: float) -> float:
7 """Calculate population density (individuals/km²)"""
8 return count / area1# Basic types
2name: str = "Lion"
3age: int = 5
4weight: float = 190.5
5is_dangerous: bool = True
6
7# Python 3.9+ - lowercase!
8animals: list = ["Lion", "Elephant"]
9habitat_map: dict = {"Lion": "savanna"}
10
11# Python 3.10+ - even better with specific types
12animals: list[str] = ["Lion", "Elephant", "Giraffe"]
13populations: dict[str, int] = {"Lion": 500, "Elephant": 300}
14coordinates: tuple[float, float] = (51.5, -0.1)
15unique_species: set[str] = {"Lion", "Elephant"}1def greet(name: str) -> str:
2 """Takes str, returns str"""
3 return f"Hello, {name}!"
4
5def add_numbers(a: int, b: int) -> int:
6 """Takes two ints, returns int"""
7 return a + b
8
9def calculate_average(numbers: list[float]) -> float:
10 """Takes a list of floats, returns float"""
11 return sum(numbers) / len(numbers)
12
13def print_info(message: str) -> None:
14 """None means 'returns nothing' """
15 print(message)
16 # no return or return Nonetyping module (Python 3.9+)1from typing import Optional
2
3def find_species(name: str) -> Optional[str]:
4 """
5 Returns species or None if not found
6
7 Optional[str] = str | None
8 """
9 database = {"Lion": "Panthera leo", "Elephant": "Loxodonta africana"}
10 return database.get(name) # May return str or None
11
12result = find_species("Lion") # Optional[str]
13if result is not None:
14 print(f"Found: {result}")1from typing import Union
2
3def process_id(animal_id: Union[int, str]) -> str:
4 """
5 Accepts int OR str
6
7 Python 3.10+: int | str
8 """
9 return f"ID: {animal_id}"
10
11process_id(123) # OK - int
12process_id("LEO-45") # OK - strPython 3.10+: You can use
| instead of Union!1def process_id(animal_id: int | str) -> str:
2 """Newer syntax - Python 3.10+"""
3 return f"ID: {animal_id}"
4
5# Optional can also be written shorter
6def find_species(name: str) -> str | None:
7 """str | None = Optional[str]"""
8 pass1# Python 3.9+ - use lowercase!
2def process_animals(names: list[str]) -> dict[str, int]:
3 """List of strings → dictionary string:int"""
4 return {name: len(name) for name in names}
5
6# Tuple - fixed number of elements with specified types
7def get_coordinates() -> tuple[float, float]:
8 """Returns (latitude, longitude)"""
9 return (51.5074, -0.1278)
10
11# Tuple - variable number of elements of the same type
12def get_observations() -> tuple[int, ...]:
13 """Tuple with any number of ints"""
14 return (12, 45, 23, 67, 89)
15
16# Set
17def get_unique_habitats() -> set[str]:
18 """Set of unique habitats"""
19 return {"savanna", "jungle", "mountains"}1from typing import Any
2
3def log_data(data: Any) -> None:
4 """Accepts anything - use sparingly!"""
5 print(f"Logging: {data}")
6
7log_data(123) # OK
8log_data("text") # OK
9log_data([1, 2, 3]) # OK
10log_data({"key": "val"}) # OKNote: Use
Any only when you truly accept any type! It's like giving up on type hints.1class Species:
2 """Species with type hints"""
3
4 # Class attributes
5 kingdom: str = "Animalia"
6 all_species: list['Species'] = [] # Forward reference
7
8 def __init__(
9 self,
10 scientific_name: str,
11 common_name: str,
12 population: int,
13 endangered: bool = False
14 ) -> None:
15 """Constructor with type hints"""
16 self.scientific_name: str = scientific_name
17 self.common_name: str = common_name
18 self.population: int = population
19 self.endangered: bool = endangered
20 self.observations: list[dict[str, Any]] = []
21
22 Species.all_species.append(self)
23
24 def add_observation(
25 self,
26 location: str,
27 count: int,
28 date: str | None = None
29 ) -> None:
30 """Add observation - None return type"""
31 obs: dict[str, str | int] = {
32 "location": location,
33 "count": count
34 }
35 if date:
36 obs["date"] = date
37 self.observations.append(obs)
38
39 def get_population_trend(self) -> float:
40 """Returns float"""
41 if len(self.observations) < 2:
42 return 0.0
43
44 first = self.observations[0]["count"]
45 last = self.observations[-1]["count"]
46 return (last - first) / first * 100
47
48 @classmethod
49 def get_endangered_species(cls) -> list['Species']:
50 """Returns list of Species - forward reference!"""
51 return [s for s in cls.all_species if s.endangered]
52
53 @staticmethod
54 def is_valid_population(pop: int) -> bool:
55 """Returns bool"""
56 return pop >= 01from typing import Callable
2
3def apply_filter(
4 species_list: list[Species],
5 filter_func: Callable[[Species], bool]
6) -> list[Species]:
7 """
8 Filter list by function
9
10 Callable[[Species], bool] = function taking Species, returning bool
11 """
12 return [s for s in species_list if filter_func(s)]
13
14# Usage
15def is_endangered(species: Species) -> bool:
16 return species.endangered
17
18endangered = apply_filter(all_species, is_endangered)1from typing import TypeAlias
2
3# Alias for readability
4PopulationData: TypeAlias = dict[str, list[int]]
5Coordinates: TypeAlias = tuple[float, float]
6SpeciesDict: TypeAlias = dict[str, Species]
7
8def analyze_populations(data: PopulationData) -> float:
9 """The PopulationData type is more readable than dict[str, list[int]]"""
10 all_counts: list[int] = []
11 for counts in data.values():
12 all_counts.extend(counts)
13 return sum(all_counts) / len(all_counts)
14
15def get_location(name: str) -> Coordinates:
16 """Coordinates clearly states what we return"""
17 locations: dict[str, Coordinates] = {
18 "Serengeti": (-2.3333, 34.8333),
19 "Masai Mara": (-1.5, 35.1667)
20 }
21 return locations.get(name, (0.0, 0.0))1from typing import Generic, TypeVar
2
3T = TypeVar('T') # Generic type
4
5class Stack(Generic[T]):
6 """Generic stack - can store any type"""
7
8 def __init__(self) -> None:
9 self._items: list[T] = []
10
11 def push(self, item: T) -> None:
12 """Add element of type T"""
13 self._items.append(item)
14
15 def pop(self) -> T:
16 """Remove and return element of type T"""
17 return self._items.pop()
18
19 def is_empty(self) -> bool:
20 return len(self._items) == 0
21
22# Usage
23string_stack: Stack[str] = Stack[str]()
24string_stack.push("Lion")
25string_stack.push("Elephant")
26animal: str = string_stack.pop() # IDE knows it's str!
27
28int_stack: Stack[int] = Stack[int]()
29int_stack.push(123)
30int_stack.push(456)
31number: int = int_stack.pop() # IDE knows it's int!1from typing import Optional, TypeAlias
2from datetime import date
3from enum import Enum
4
5# === TYPE ALIASES ===
6
7SpeciesID: TypeAlias = str
8Coordinates: TypeAlias = tuple[float, float]
9ObservationData: TypeAlias = dict[str, str | int | date]
10
11# === ENUMS ===
12
13class ConservationStatus(Enum):
14 """Species conservation status"""
15 EXTINCT = "extinct"
16 EXTINCT_IN_WILD = "extinct_in_wild"
17 CRITICALLY_ENDANGERED = "critically_endangered"
18 ENDANGERED = "endangered"
19 VULNERABLE = "vulnerable"
20 NEAR_THREATENED = "near_threatened"
21 LEAST_CONCERN = "least_concern"
22
23class Habitat(Enum):
24 """Habitat type"""
25 SAVANNA = "savanna"
26 JUNGLE = "jungle"
27 MOUNTAINS = "mountains"
28 DESERT = "desert"
29 WETLANDS = "wetlands"
30
31# === MAIN CLASS ===
32
33class Species:
34 """
35 Species class with full type hints
36
37 Precise classification like in biology!
38 """
39
40 # Class attribute
41 _registry: dict[SpeciesID, 'Species'] = {}
42
43 def __init__(
44 self,
45 scientific_name: str,
46 common_name: str,
47 population: int,
48 habitat: Habitat,
49 status: ConservationStatus,
50 dangerous: bool = False
51 ) -> None:
52 """
53 Initialize species
54
55 Args:
56 scientific_name: Scientific name (e.g., "Panthera leo")
57 common_name: Common name (e.g., "Lion")
58 population: Number of individuals
59 habitat: Habitat type (enum)
60 status: Conservation status (enum)
61 dangerous: Whether dangerous to humans
62 """
63 self.id: SpeciesID = scientific_name
64 self.scientific_name: str = scientific_name
65 self.common_name: str = common_name
66 self.population: int = population
67 self.habitat: Habitat = habitat
68 self.status: ConservationStatus = status
69 self.dangerous: bool = dangerous
70
71 # Observations list
72 self.observations: list[ObservationData] = []
73
74 # Locations
75 self.locations: set[Coordinates] = set()
76
77 # Register
78 Species._registry[self.id] = self
79
80 def add_observation(
81 self,
82 location: str,
83 coordinates: Coordinates,
84 count: int,
85 observation_date: date | None = None
86 ) -> None:
87 """
88 Add a species observation
89
90 Args:
91 location: Location name
92 coordinates: (latitude, longitude)
93 count: Number of observed individuals
94 observation_date: Observation date (optional)
95 """
96 obs: ObservationData = {
97 "location": location,
98 "count": count,
99 "date": observation_date or date.today()
100 }
101 self.observations.append(obs)
102 self.locations.add(coordinates)
103
104 def get_total_observed(self) -> int:
105 """Return total number of observed individuals"""
106 return sum(
107 int(obs["count"])
108 for obs in self.observations
109 )
110
111 def get_observation_locations(self) -> list[str]:
112 """Return list of unique observation locations"""
113 locations: set[str] = {
114 str(obs["location"])
115 for obs in self.observations
116 }
117 return sorted(locations)
118
119 def is_threatened(self) -> bool:
120 """Check if species is threatened"""
121 threatened_statuses: set[ConservationStatus] = {
122 ConservationStatus.CRITICALLY_ENDANGERED,
123 ConservationStatus.ENDANGERED,
124 ConservationStatus.VULNERABLE
125 }
126 return self.status in threatened_statuses
127
128 def get_risk_assessment(self) -> dict[str, str | int | bool]:
129 """
130 Risk assessment for the expedition
131
132 Returns:
133 Dictionary with risk assessment
134 """
135 risk_level: int = 0
136
137 if self.dangerous:
138 risk_level += 5
139
140 if self.population < 100:
141 risk_level += 3 # Rare - hard to find
142
143 if self.habitat == Habitat.JUNGLE:
144 risk_level += 2 # Difficult terrain
145
146 return {
147 "species": self.common_name,
148 "risk_level": min(10, risk_level),
149 "dangerous": self.dangerous,
150 "rare": self.population < 100,
151 "recommendation": "Extreme caution" if risk_level >= 7 else "Normal protocol"
152 }
153
154 @classmethod
155 def get_by_id(cls, species_id: SpeciesID) -> Optional['Species']:
156 """
157 Find species by ID
158
159 Returns:
160 Species or None if not found
161 """
162 return cls._registry.get(species_id)
163
164 @classmethod
165 def get_by_habitat(cls, habitat: Habitat) -> list['Species']:
166 """Return all species from a given habitat"""
167 return [
168 species for species in cls._registry.values()
169 if species.habitat == habitat
170 ]
171
172 @classmethod
173 def get_endangered(cls) -> list['Species']:
174 """Return all threatened species"""
175 return [
176 species for species in cls._registry.values()
177 if species.is_threatened()
178 ]
179
180 @staticmethod
181 def calculate_biodiversity_index(
182 species_list: list['Species']
183 ) -> float:
184 """
185 Calculate Simpson's biodiversity index
186
187 Args:
188 species_list: List of species to analyze
189
190 Returns:
191 Index (0.0 - 1.0, higher = greater diversity)
192 """
193 if not species_list:
194 return 0.0
195
196 total: int = sum(s.population for s in species_list)
197 if total == 0:
198 return 0.0
199
200 sum_squares: float = sum(
201 (s.population / total) ** 2
202 for s in species_list
203 )
204
205 return 1.0 - sum_squares
206
207 def __str__(self) -> str:
208 return f"{self.common_name} ({self.population} individuals)"
209
210 def __repr__(self) -> str:
211 return (f"Species(scientific_name='{self.scientific_name}', "
212 f"population={self.population}, "
213 f"habitat={self.habitat.value})")
214
215# === HELPER FUNCTIONS ===
216
217def generate_report(species: Species) -> str:
218 """
219 Generate a species report
220
221 Args:
222 species: Species to report on
223
224 Returns:
225 Formatted text report
226 """
227 threatened: str = "⚠️ YES" if species.is_threatened() else "✓ No"
228 risk: dict[str, str | int | bool] = species.get_risk_assessment()
229
230 report: str = f"""
231╔══════════════════════════════════════════════════╗
232 {species.common_name.upper()}
233╠══════════════════════════════════════════════════╣
234 Scientific name: {species.scientific_name}
235 Population: {species.population} individuals
236 Habitat: {species.habitat.value}
237 Status: {species.status.value}
238 Threatened: {threatened}
239 Dangerous: {"⚠️ YES" if species.dangerous else "✓ No"}
240 Risk level: {risk['risk_level']}/10
241 Observations: {len(species.observations)}
242 Locations: {', '.join(species.get_observation_locations()) or 'None'}
243╚══════════════════════════════════════════════════╝
244 """.strip()
245
246 return report
247
248def filter_by_population(
249 species_list: list[Species],
250 min_pop: int,
251 max_pop: int | None = None
252) -> list[Species]:
253 """
254 Filter species by population
255
256 Args:
257 species_list: List of species
258 min_pop: Minimum population
259 max_pop: Maximum population (optional)
260
261 Returns:
262 Filtered list
263 """
264 filtered: list[Species] = [
265 s for s in species_list
266 if s.population >= min_pop
267 ]
268
269 if max_pop is not None:
270 filtered = [s for s in filtered if s.population <= max_pop]
271
272 return filtered
273
274# === DEMONSTRATION ===
275
276print("=== SPECIES CATALOG WITH TYPE HINTS ===\n")
277
278# Creating species
279lion = Species(
280 scientific_name="Panthera leo",
281 common_name="Lion",
282 population=120,
283 habitat=Habitat.SAVANNA,
284 status=ConservationStatus.VULNERABLE,
285 dangerous=True
286)
287
288rhino = Species(
289 scientific_name="Diceros bicornis",
290 common_name="Black Rhinoceros",
291 population=45,
292 habitat=Habitat.SAVANNA,
293 status=ConservationStatus.CRITICALLY_ENDANGERED,
294 dangerous=True
295)
296
297elephant = Species(
298 scientific_name="Loxodonta africana",
299 common_name="African Elephant",
300 population=450,
301 habitat=Habitat.SAVANNA,
302 status=ConservationStatus.ENDANGERED,
303 dangerous=False
304)
305
306# Add observations
307lion.add_observation("Serengeti", (-2.3333, 34.8333), 12)
308lion.add_observation("Masai Mara", (-1.5, 35.1667), 8)
309rhino.add_observation("Ngorongoro", (-3.1792, 35.5500), 3)
310elephant.add_observation("Amboseli", (-2.6527, 37.2606), 35)
311
312# Reports
313print(generate_report(lion))
314print()
315print(generate_report(rhino))
316
317# Filtering
318print("\n=== THREATENED SPECIES ===")
319endangered: list[Species] = Species.get_endangered()
320for species in endangered:
321 print(f" ⚠️ {species.common_name}: {species.status.value}")
322
323# Filter by population
324print("\n=== SPECIES WITH POPULATION < 100 ===")
325rare: list[Species] = filter_by_population(
326 list(Species._registry.values()),
327 min_pop=0,
328 max_pop=99
329)
330for species in rare:
331 print(f" - {species.common_name}: {species.population} individuals")
332
333# Biodiversity index
334biodiversity: float = Species.calculate_biodiversity_index(
335 list(Species._registry.values())
336)
337print(f"\nBiodiversity index: {biodiversity:.3f}")1# Installation
2pip install mypy
3
4# Check a file
5mypy script.py
6
7# Example error
8def add(a: int, b: int) -> int:
9 return a + b
10
11result: str = add(5, 10) # mypy ERROR: incompatible types!Modern IDEs (VS Code, PyCharm) automatically check type hints:
1# ✅ Good - public API with type hints
2class Species:
3 def add_observation(self, location: str, count: int) -> None:
4 pass
5
6# ❌ Bad - public API without types
7class Species:
8 def add_observation(self, location, count):
9 pass1# ❌ Bad - too generic
2def process_data(data: Any) -> Any:
3 return data
4
5# ✅ Good - specific types
6def process_data(data: dict[str, int]) -> list[int]:
7 return list(data.values())1# ✅ Good - clearly specified
2def find_species(name: str) -> Species | None:
3 pass
4
5# ❌ Misleading - does it return None?
6def find_species(name: str) -> Species:
7 pass1# ✅ Readable
2ObservationData: TypeAlias = dict[str, str | int | date]
3
4def add_observation(data: ObservationData) -> None:
5 pass
6
7# ❌ Unreadable
8def add_observation(data: dict[str, str | int | date]) -> None:
9 passIn this lesson you learned:
| syntax in Python 3.10+)Before moving on:
|)Safari Analogy: Type hints are like precise biological classification - instead of "big cat" we say "Panthera leo, male, 5 years, 190kg" - everything is clear! 📝🔍
In the next lesson Darwin will teach you decorators - a powerful tool for modifying function and class behaviors! ✨🎭