The Author: [Jonas Reinhardt](https://reinhardt.ai) # The Why In the first half of 2021 I began using Python for developing backend-infrastructure after mainly being used to Java and C# for backend-development. The reason being that I had to build a full-fledged array of microservices around a machine learning component to be integrated into an existing environment of C#-based services. During that time, I learned a lot on how to build complex backend software in Python. Today, one of the concerns (and probably the most prominent one) I stumble across when people hear me talk about developing these kind of systems in Python is: *"Python is dynamically typed, you can't develop big software systems without static typing.”* I usually answer the following *“**Yes!** **I agree that code becomes difficult to read once you disregard typing.** **However,** y**ou can**, **and you should only use Python with typing**”* When it comes to using typing, discipline is required. Typing in Python is entirely optional - the CPython interpreter will attempt to run any line of code, beit typed or not. I have had very good experiences in the past with simply staying disciplined as an (admittedly small) team when writing Python code. # The Rule The Rule that I use on “when to use typing” is simple: > [!note] > **“Always use typing whenever it would be required by a staticically typed language, unless the type is absolutely obvious by the current context”** This rule is simple to remember and simple to adhere to as long as you are familiar with programming in statically typed languages, such as C#, Java or TypeScript. # The Benefits Following this rule makes your code readable in a sense that - you will never wonder what a certain variable is within a certain context, - you can always navigate to the implementation of the type, - you always know what a function or method accepts and what it returns - you will get tremendous improvements when working with an IDE (think code completion and warnings when attempting to use the wrong type) - you can conveniently use generic classes - you can conveniently use type aliases which are of great convenience when writing domain code # The How The following is supposed to be a be a convenient overview/cheat sheet on how to use typing in Python. You can find more details explanations in the official documentation: [typing - Support for type hints - Python 3.11.0 documentation](https://docs.python.org/3/library/typing.html#) > [!info] > I will use Python 3.11 compatible syntax for the following examples. There have been some improvements in terms of typing, so make sure you have a current python version if you want to try it out ## Typing Primitives Simply typing primitive variables is quite trivial ```python my_age: int = 26 my_name: str = 'Jonas' pi: float = 3.141 married: bool = False ``` ## Unions If you want to say that a variable can contain two different types of objects, this is called a union type. ```python from decimal import Decimal my_bank_account_balance: float | Decimal = Decimal(2.50) ``` ## Any If you want to explicitly type that you don’t want to specify the type, you can use `Any` . It can be used to mute IDE warnings and should be used rarely. There are barely any reasons to have something that can be anything. ## None `None` can be used like `void` in other languages for functions that return nothing or to specify optional (nullable) Types. ```python my_middle_name: str | None = 'Daniel' # not everyone has a middle name, so it might be None ``` > [!warning] > If you don’t make a type explicitly optional, the IDE will assume that it will never be `None` and won’t warn you if you don’t null-check ## Typing Data Structures Builtin data structures use generics so that they can be conveniently typed. ### Lists Let’s assume that we have a `Fruit` base-class and three subclasses called `Apple` , `Banana` , and `Cherry` . A list of `Fruit` -instances would look like this: ```python fruits: list[Fruit] = [Apple(), Banana(), Cherry()] ``` ### Dicts Dics allow you to specify the type of the keys and the type of the values ```python fruit_delivery: dict[str, int] = { 'apple': 10, 'banana': 25, 'cherry': 150 } ``` ### Going one step further: TypedDicts If you want to tie down the allowed keys and their corresponding type, you can use the `TypedDict` . This comes in handy when typing API-Endpoints ```python from typing import TypedDict class FruitDelivery(TypedDict): apple: int banana: int cherry: int ``` Now, if you were to write something like this, you would not get a warning ```python fruit_delivery: dict[str, int] = { 'apple': 10, 'banana': 25, 'chicken': 150 } ``` But if you use the `FruitDelivery` -Type, the IDE would tell you that something is wrong! ```python fruit_delivery: FruitDelivery = { 'apple': 10, 'banana': 25, 'chicken': 150 } # TypedDict 'FruitDelivery' has missing key: 'cherry' # Extra key 'chicken' for TypedDict 'FruitDelivery' ``` The same goes not only for setting dict-values, but also for accessing them ```python fruit_delivery['cherry'] # ok fruit_delivery['chicken'] # TypedDict "FruitDelivery" has no key 'chicken' ``` ### Tuples Tuples are immutable lists of objects. If you expect a tuple to be of a certain length and to contain elements of a certain type, you can write it like this ```python fruit_record: tuple[str, Fruit, amount] = ( 'apple', Apple(), 10 ) ``` If you want to use tuples with elements of a certain type and an arbitrary amount of elements you can write ```python fruits: tuple[Fruit, ...] = ( Apple(), Banana(), Cherry() ) # ... and so on ``` ### Sets A set is unordered collection of objects. Typing them works as you would expect by now ```python fruits: set[Fruit] = { Apple(), Banana(), Cherry() } # ... and so on ``` ## Typing Functions & Methods Typing becomes more interesting when we bring functions and (with classes also) methods into play. ```python class FruitSalad: # typing arguments def __init__(self, apple: Apple, banana: Banana, cherry: Cherry): self.apple = apple self.banana = banana self.cherry = cherry # typing the return value def make_fruit_salad(fruits: list[Fruit]) -> FruitSalad: ... ``` ### Going one step further: Callables If you are used to functional programming, you should be used to treat function as values just like objects or primitive variables. The type of the above `make_fruit_salad`-function would be written like ```python from typing import Callable # function of (list[Fruit]) -> FruitSalad FruitFactory = Callable[[list[Fruit]], FruitSalad] ``` ## Using “OOP” Interfaces Coming from a object oriented language you might try to find something like `interface` . Intefaces are used to achieve decoupling between classes and classes using them. But since Python doesn’t need direct references to the classes that a certain function uses, due to its dynamic nature, we have to find a solution to a self-made problem. Let’s quickly head over to TypeScript and look at how we would decouple our `Fruit` -implementation from the `make_fruit_salad` factory function. ```tsx interface Fruit { hasKernels: bool; peel(): void; } class FruitImpl implements Fruit { ... } function make_fruit_salad(fruits: Fruit[]): FruitSalad { fruit = fruits[0]; console.log(fruit.hasKernels); fruit.peel(); ... } ``` Now, whenever we use the function we only need to know about the `Fruit`-interface, not about any `Fruit`-implementation. In Python, we can do the same thing! We just need to change the synax a little bit: ```python from abc import abstractmethod, ABC class Fruit(ABC): @abstractmethod @property def has_kernels() -> bool: ... @abstractmethod def peel() -> None: ... class FruitImpl(Fruit): ... def make_fruit_salad(fruits: list[Fruit]): FruitSalad: fruit = fruits[0] print(fruit.has_kernels) fruit.peel() ... ``` The best way to understand this construct is to acknowledge that interfaces *are* abstract classes which contain only abstract methods and properties. Now we can make the `Fruit`-interface a abstract class. We have to use the `@abstractmethod` -decorater to make it abstract. For properties we have to additionally use the `@property` -decorator, because otherwise it would simply be a method. ## Generics We have already used generics for collections. For example, we expect a `list[Fruit]` to be a list that contains only fruits. In this case `Fruit` is the generic type. We can use generics in classes to statically define on which what kind of objects a class can be used. For instance, let’s say we have a `FruitDeliveryQueue` in our little domain. ```python TFruit = TypeVar('TFruit', bound=Fruit) class FruitDeliveryQueue(Generic[TFruit]): def pop() -> TFruit: ... def push(fruit: TFruit) -> None: ... apple_queue: FruitDeliveryQueue[Apple] = FruitDeliveryQueue() ``` Obviously, we can also use generics in functions to statically define corresponding input- and output types. For instance, if we implement our fruit salad creation process in a less object-oriented and more procedual way, we might have something like ```python def peel(fruit: TFruit) -> TFruit: ... ``` That way, when you put an apple into your `peel` -function, you can be sure to get an apple back and not suddenly a banana (which could happen if you just had a `Fruit -> Fruit` -function). ## Using Type Aliases This is once again a practise more used in the world of functional programming. You don’t need complex inheritance hierarchies or even classes to model a domain. If in the end you decide that a `FruitSalad` ist just a randomly ordered collection of individual fruits, then well, make it so ```python FruitSalad = set[Fruit] ``` That’s it. This is especially helpful for data that on one hand are close to the domain, but can be represented by simple data types or for aliasing types that would be to long to be used for type hints, such as our `FruitFactory` from earlier. You don’t always need a class. # The Exception Whenever there is a rule, there must be an exeption (otherwise it would be called an Axiom). In my opinion, you can disregard typing as long as - You write small python programs, that are so simple that they can be well understood without using typing - You are working within in a personal project and don’t need anyone else to read and understand the code > [!warning] > However, be careful when you disobey the rule for your own convenience. Humans are animals of habit. And habits will always form when you keep doing things a certain way. It will become unnecessarily hard to break habits for a future project just because you wanted to be a few seconds quicker right now.