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.