Your report function needs one number from your metrics database:
from metrics_db import PostgresMetrics
def metric_label(db: PostgresMetrics, name: str) -> str:
return f"{name}: {db.latest(name):.2f}"
To test it, you need a PostgresMetrics, and importing the report module
loads the database driver. The function calls one method, latest, but its
signature asks for the whole client.
You met Protocol in the interfaces track. It is a class that lists the
methods a parameter needs, and any object with those methods fits, without
inheriting from it. Use one to ask for only what the function uses.
Define the Protocol next to the code that uses it
from typing import Protocol
class MetricSource(Protocol):
def latest(self, name: str) -> float: ...
def metric_label(source: MetricSource, name: str) -> str:
return f"{name}: {source.latest(name):.2f}"
class MemoryMetrics:
def __init__(self, values: dict[str, float]) -> None:
self.values = values
def latest(self, name: str) -> float:
return self.values[name]
print(metric_label(MemoryMetrics({"loss": 0.125}), "loss")) # loss: 0.12
MemoryMetrics does not inherit from MetricSource. When you pass one to
metric_label, mypy checks that its latest method matches the Protocol. If
latest returned str, mypy would reject that call.
Put MetricSource in the reporting module, or in a small module of its own
that does not import the database driver. PostgresMetrics lives elsewhere, and it does not
need to import the Protocol at all. The report module now imports without the
database driver, and its test runs with MemoryMetrics.
The next articles build on this. The code that uses a dependency states the Protocol it needs. The code that talks to the database only has to have the right methods.
Use a standard type when one fits
For a function that only looks up values, collections.abc.Mapping already
says what it needs:
from collections.abc import Mapping
from types import MappingProxyType
def completed(counts: Mapping[str, int]) -> int:
return counts.get("completed", 0)
counts = {"completed": 3}
print(completed(counts)) # 3
print(completed(MappingProxyType(counts))) # 3
It accepts a dict and a read-only view of one. If the function writes
entries, ask for MutableMapping instead.
Check your understanding mypy accepts the object you pass. Does that mean it behaves the way the function expects? Show answer Hide answer
No. mypy checks method names and types, not behavior. A MetricSource can
return an old value, or raise for a name it does not know. Write those rules
in the Protocol's docstring, and test each implementation against them. mypy
also checks nothing at runtime, so an object from untyped code can still lack
the method.
See the typing specification on protocols.