Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Enforcing units on numbers using Python type hints

Is there a way to use Python type hints as units? The type hint docs show some examples that suggest it might be possible using NewType, but also those examples show that addition of two values of the same "new type" do not give a result of the "new type" but rather the base type. Is there a way to enrich the type definition so that you can specify type hints that work like units (not insofar as they convert, but just so that you get a type warning when you get a different unit)? Something that would allow me to do this or similar:

Seconds = UnitType('Seconds', float)
Meters = UnitType('Meters', float)

time1 = Seconds(5)+ Seconds(8) # gives a value of type `Seconds`
bad_units1 = Seconds(1) + Meters(5) # gives a type hint error, but probably works at runtime
time2 = Seconds(1)*5 # equivalent to `Seconds(1*5)` 
# Multiplying units together of course get tricky, so I'm not concerned about that now.

I know runtime libraries for units exist, but my curiosity is if type hints in python are capable of handling some of that functionality.

like image 973
Garrett Motzner Avatar asked Jun 26 '20 22:06

Garrett Motzner


People also ask

Does Python enforce type hints?

The Python runtime does not enforce function and variable type annotations. They can be used by third party tools such as type checkers, IDEs, linters, etc. This module provides runtime support for type hints. The most fundamental support consists of the types Any , Union , Callable , TypeVar , and Generic .

What are type hints in Python?

Python's type hints provide you with optional static typing to leverage the best of both static and dynamic typing. Besides the str type, you can use other built-in types such as int , float , bool , and bytes for type hintings. To check the syntax for type hints, you need to use a static type checker tool.

Should I use type hinting in Python?

Type hints work best in modern Pythons. Annotations were introduced in Python 3.0, and it's possible to use type comments in Python 2.7. Still, improvements like variable annotations and postponed evaluation of type hints mean that you'll have a better experience doing type checks using Python 3.6 or even Python 3.7.

Does Python make hinting faster?

PEP 484 introduced type hints — a way to make Python feel statically typed. While type hints can help structure your projects better, they are just that — hints — and by default do not affect the runtime.


2 Answers

You can do this by creating a type stub file, which defines the acceptable types for the __add__/__radd__ methods (which define the + operator) and __sub__/__rsub__ methods (which define the - operator). There are many more similar methods for other operators of course, but for the sake of brevity this example only uses those.

units.py

Here we define the units as simple aliases of int. This minimises the runtime cost, since we aren't actually creating a new class.

Seconds = int
Meters = int

units.pyi

This is a type stub file. It tells type checkers the types of everything defined in units.py, instead of having the types defined within the code there. Type checkers assume this is the source of truth, and don't raise errors when it differs from what is actually defined in units.py.

from typing import Generic, TypeVar

T = TypeVar("T")

class Unit(int, Generic[T]):
    def __add__(self, other: T) -> T: ...
    def __radd__(self, other: T) -> T: ...
    def __sub__(self, other: T) -> T: ...
    def __rsub__(self, other: T) -> T: ...
    def __mul__(self, other: int) -> T: ...
    def __rmul__(self, other: int) -> T: ...

class Seconds(Unit["Seconds"]): ...

class Meters(Unit["Meters"]): ...

Here we define Unit as a generic type inheriting from int, where adding/subtracting takes and returns values of type parameter T. Seconds and Meters are then defined as subclasses of Unit, with T equal to Seconds and Meters respectively.

This way, the type checker knows that adding/subtracting with Seconds takes and returns other values of type Seconds, and similarly for Meters.

Also, we define __mul__ and __rmul__ on Unit as taking a parameter of type int and returning T - so Seconds(1) * 5 should have type Seconds.

main.py

This is your code.

from units import Seconds, Meters

time1 = Seconds(5) + Seconds(8)
# time1 has type Seconds, yay!

bad_units1 = Seconds(1) + Meters(5)
# I get a type checking error:
# Operator "+" not supported for types "Meters" and "Seconds"
# Yay!

time2 = Seconds(1) * 5
# time2 has type Seconds, yay!

meter_seconds = Seconds(1) * Meters(5)
# This is valid because `Meters` is a subclass of `int` (as far
# as the type checker is concerned). meter_seconds ends up being
# type Seconds though - as you say, multiplying gets tricky.

Of course, all of this is just type checking. You can do what you like at run time, and the pyi file won't even be loaded.

like image 61
Artemis Avatar answered Sep 22 '22 20:09

Artemis


The answer from @Artemis is excellent, but throws error when used with MyPy (@Artemis is using Pylance).

I made the following modifications to units.pyi (based on a suggestion from @Artemis) and it appears to be working well:

from typing import Generic, TypeVar, Union

T = TypeVar("T")

class Unit(Generic[T]):
    def __add__(self, other: Union[T, int]) -> T: ...
    def __radd__(self, other: Union[T, int]) -> T: ...
    def __sub__(self, other: Union[T, int]) -> T: ...
    def __rsub__(self, other: Union[T, int]) -> T: ...
    def __mul__(self, other: Union[T, int]) -> T: ...
    def __rmul__(self, other: Union[T, int]) -> T: ...

    def __init__(self, val: int) -> None: ...

class Seconds(Unit["Seconds"]): ...

class Meters(Unit["Meters"]): ...

The only hold-up is that you must create values using

v: Seconds = Seconds(1)

rather than:

v: Seconds = 1

Other than that, MyPy is able to catch operations using mixed types.

like image 45
evan.oman Avatar answered Sep 20 '22 20:09

evan.oman