Is there a simple, modern, way to type hint physical units in Python 3.9+?

Viewed 51

I am faced with a problem where I have to create a lot of classes with a lot of attributes that relate to physical quantities. I use dataclass to make this easy. However, these attributes can relate to different physical units, and I would like to add these units as type hints. I have tried using Annotated, NewType and TypeVar, but none of these solutions passes mypy:

units.py

from dataclasses import dataclass
from typing import Annotated, NewType, TypeVar

meter = TypeVar('meter', bound=float)
um = Annotated[float, 'um']
ohm = NewType('ohm', float)


@dataclass
class Data:
    length_1: meter = 10e-3
    length_2: um = 20
    R: ohm = 50

> mypy units.py:

units.py:11: error: Type variable "units.meter" is unbound  [valid-type]
units.py:11: note: (Hint: Use "Generic[meter]" or "Protocol[meter]" base class to bind "meter" inside a class)
units.py:11: note: (Hint: Use "meter" in function signature to bind "meter" inside a function)
units.py:12: error: Variable "units.um" is not valid as a type  [valid-type]
units.py:12: note: See https://mypy.readthedocs.io/en/latest/common_issues.html#variables-vs-type-aliases
units.py:13: error: Incompatible types in assignment (expression has type "int", variable has type "ohm")  [assignment]

I understand that I misuse the typing module slightly in all cases here, but is there any way to achieve anything like this that passes mypy? I would like to pass a float to the dataclass __init__ and not another special type, and then build some conversion to SI-units in the __post_init__ of the dataclass using the type types specified in the class definition, or something similar. The reason is mainly to minimize code redundancy, since I will have to write a lot of generic classes, and I want the readability to be high.

1 Answers

I would declare a new type that inherits from the built-in type you want to use to represent it. e.g.

class Meter(float):
    ...

You can then use this in your data class as follows:

@dataclass
class Data:
    length_1: Meter = Meter(10e-3)
Related