indusai.co

Type hints and dataclasses

Type hints let you write down what types a function expects and returns. Python does not enforce them at runtime, but editors use them for autocomplete and warnings, and tools like mypy check them before you run anything. Dataclasses build on hints to remove boilerplate from simple classes.

Annotating variables and functions

python
def greet(name: str, times: int = 1) -> str:
    return ("Hello, " + name + " ") * times

count: int = 3
print(greet("Ravi", count))

The syntax is name: type for parameters and variables, and -> type for the return value.

Collection types

python
def average(values: list[float]) -> float:
    return sum(values) / len(values)

def index_by_id(rows: list[dict[str, str]]) -> dict[str, dict[str, str]]:
    return {row["id"]: row for row in rows}

print(average([1.5, 2.5]))
print(index_by_id([{"id": "a", "v": "1"}]))

Since Python 3.9 you can use the built in names list, dict, tuple, set directly with square brackets.

Optional and union

python
def find(items: list[str], target: str) -> int | None:
    return items.index(target) if target in items else None

print(find(["a", "b"], "b"))
print(find(["a", "b"], "z"))

int | None means "an int or None". Older code writes Optional[int] from the typing module.

Other useful hints

python
from typing import Any, Callable, Iterable

def apply(f: Callable[[int], int], values: Iterable[int]) -> list[int]:
    return [f(v) for v in values]

def log(payload: Any) -> None:
    print(payload)

print(apply(lambda x: x * 2, range(4)))

Type aliases

python
Point = tuple[float, float]

def distance(a: Point, b: Point) -> float:
    return ((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2) ** 0.5

print(distance((0, 0), (3, 4)))

Hints are not enforced

python
def double(n: int) -> int:
    return n * 2

print(double("ab"))    # runs fine, the hint is only advice

Run mypy yourfile.py to have the mismatch reported before it becomes a bug.

Dataclasses

For classes that mainly hold data, @dataclass writes __init__, __repr__ and __eq__ for you from the annotated fields.

python
from dataclasses import dataclass, field

@dataclass
class Student:
    name: str
    marks: list[int] = field(default_factory=list)
    active: bool = True

    def average(self) -> float:
        return sum(self.marks) / len(self.marks) if self.marks else 0.0

s = Student("Neha", [88, 92])
print(s)
print(s.average())
print(s == Student("Neha", [88, 92]))

Use field(default_factory=list) for mutable defaults; a plain = [] would be shared between instances.

Frozen dataclasses

frozen=True makes instances immutable and hashable, so they can be dictionary keys.

python
from dataclasses import dataclass

@dataclass(frozen=True)
class Coord:
    lat: float
    lon: float

c = Coord(26.9, 75.8)
print({c: "Jaipur"}[c])
try:
    c.lat = 0
except Exception as e:
    print(type(e).__name__)

Ordering

order=True generates comparison methods field by field.

python
from dataclasses import dataclass

@dataclass(order=True)
class Version:
    major: int
    minor: int

print(sorted([Version(1, 10), Version(1, 2), Version(0, 9)]))

Practice

  1. Add type hints to a function that takes a list of names and returns a dictionary from name to its length.
  2. Write a Book dataclass with title, author, year and an optional list of tags. Print two instances and compare them.
  3. Make a frozen Money dataclass with amount and currency and use it as a dictionary key.