We use cookies to enhance your experience on the site
CodeWorlds

Type hints - precise classification

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)

What are type hints?

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:

  • 📚 Documentation - explain the programmer's intentions
  • 🔍 IDE - better suggestions and autocomplete
  • 🐛 Tools - mypy, pyright can detect type errors
  • 🧠 Readability - easier to understand code
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 / area

Basic type hints

Variables

1# 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"}

Functions - parameters and return type

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 None

Types from the
typing
module (Python 3.9+)

Optional - can be None

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

Union - one of several types

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 - str

Python 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    pass

List, Dict, Tuple, Set - with specific types

1# 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"}

Any - any type

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"})  # OK

Note: Use

Any
only when you truly accept any type! It's like giving up on type hints.

Type hints for classes

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

Advanced type hints

Callable - functions as types

1from 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)

TypeAlias - aliases for complex types

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))

Generic - generic classes

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!

Safari example - complete system with type hints

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

Tools for type checking

mypy - static type checking

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!

Type checking in IDEs

Modern IDEs (VS Code, PyCharm) automatically check type hints:

  • ✅ Autocomplete - suggestions based on types
  • ✅ Error detection - detecting type errors
  • ✅ Refactoring - safer changes
  • ✅ Documentation - quick signature preview

Type hints best practices

1. Always add type hints to public APIs

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        pass

2. Use specific types instead of Any

1# ❌ 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())

3. Use Optional for None 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    pass

4. Document complex types with TypeAlias

1# ✅ 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    pass

Summary

In this lesson you learned:

  • ✅ What type hints are and why to use them
  • ✅ Basic annotations: int, str, float, bool, list, dict
  • ✅ Optional and Union (and the
    |
    syntax in Python 3.10+)
  • ✅ Type hints for functions (parameters and return types)
  • ✅ Type hints for classes and methods
  • ✅ Advanced techniques: Callable, TypeAlias, Generic
  • ✅ Enums for safe values
  • ✅ Tools like mypy
  • ✅ Type hints best practices

Checkpoint

Before moving on:

  • [ ] You understand the difference between type hints and runtime behavior
  • [ ] You can add type hints to functions
  • [ ] You know Optional and Union (and
    |
    )
  • [ ] You know how to use list[T], dict[K, V]
  • [ ] You understand TypeAlias and Generic

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! ✨🎭

Go to CodeWorlds