Learning Generics by extending the prometheus python client

Imagine you’re writing a library to extend the prometheus python client and you needed to add some dynamic labels specific to the environment. Ideally you would add most of your labels at the collector end and avoid writing extensions.

Say you need to inject some dynamic labels and you will need to at some point, we may want to extend the client.

What are Generics? and why do we use them in python?

Without sacrificing the inherent safety of a statically typed language, generic programming gives us primitives to declare “placeholder types” that allow us to focus less on the specific types that may be used or declared by other portions of the codebase, but rather focus on these higher-level patterns that can be consolidated into simpler declarations.

Python has no built-in type checking. As long as a given Python program is syntactically valid, it will run, and issues like incompatible types will only surface at runtime. This forces the developer to ensure there is error handling in place to deal with such errors, and even with this, a common best practice is to use type hints combined with third-party linting tools to try to stay on top of issues like this.

Generics in my opinion is a programming stype for statically typed languages brought into python via type hints.

Some code with comments

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from prometheus_client import Counter as _PromCounter
from prometheus_client import Histogram as _PromHistogram
from typing import TypeVar, Generics

# a new generic type, bound to two types
_MetricsTypeT = TypeVar('_MetricsTypeT', bound=[_PromCounter, _PromHistogram])

# base class of type generic(child class to pass type in)
class _MetricsBase(Generic[_MetricsTypeT]):
    def __init__(self, label_names: Iterable[str]):
        self.default_labels: Dict[str] = get_default_labels()
        self.all_label_names: list = list(label_names) + list(self.default_labels.keys())
        self._parent_metric: _MetricsTypeT = None

    # Provides the label functionality
    def labels(self, *labelargs, **labelkwargs) -> _MetricsTypeT:            
        if labelargs:
            labelargs += tuple(self.default_labels.values())
            return cast(_MetricsTypeT, self._parent_metric.labels(*labelargs))
        labelkwargs.update(self.default_labels)
        return cast(_MetricsTypeT, self._parent_metric.labels(**labelkwargs))


# child class passing in type to base class via generics
class Counter(_MetricsBase[_PromCounter]):
    def __init__(self, name, documentation, labelnames=()):
        super().__init__(label_names=labelnames)
        self._parent_metric = _PromCounter(
            name=name, documentation=documentation,
            labelnames=self.all_label_names)

Complete code here

The final world

In my project, I used generics programing to type my base class. My base clase _MetricsBase accepts a Generic type, passed in by classes(Counter, Histogram) inheritering from it. The common method label returns the generic type passed in from the child class. When we use third party linters like mypy, we get some of the controll of statically typed languages with python

End
Built with Hugo
Theme Stack designed by Jimmy