Skip to main content

mudu logo

mudu

PyPI version License: MIT CI status Python 3.12+

mudu is a package for units and dimension handling, unit conversion and unit arithmetic, with support for custom units definition.

mudu was created as part of a larger project (a final year project), flightperformance, a Python package for analyzing aircraft (fixed wing) performance. As it is standard in engineering to specify the units of data so as to ensure dimensional homogeneity and reproducibility, the flightperformance package required a means of specifying the units of data, the quantity they represent, converting between units and doing arithmetic operations effortlessly with emphasis on flexibility and readability — this is what mudu does.

mudu provides a set of classes and methods, while retaining Python's expressiveness, to add unit signatures to numeric data, convert between units and perform valid arithmetic operations on dimension (or quantities) and unit objects. At its core, mudu creates dimensions using the _DimensionUnitBase base class while units are all instances or child classes of the _UnitType class, both these classes are composed of other classes.

Why use mudu

  • It provides a simple and expressive way of specifying units
  • Capability to perform conversion from one unit to another while ensuring dimensional homogeneity as it applies in mathematics
  • Supports valid arithmetic operations
  • It provides a public interface to define custom units on existing dimensions and register conversions between custom and existing units, and to define wholly new custom quantities

This README contains a quick tutorial that captures almost all the important functionality of mudu, and some selected examples to give an easy start on using the package. Full documentation (API reference, extended examples) is at mudu.readthedocs.io.

Installation

To install using pip, run:

pip install mudu

Or clone the repository and install locally:

git clone https://github.com/techkaduna/mudu.git
cd mudu
pip install -e .

Requirements: Python ≥ 3.12, sympy, numpy.

Usage Guide

This section provides a brief but comprehensive tutorial on how to use mudu and its features. It's divided into two sub-sections:

  • Basic usage: covers the basics of specifying units and dimensions, as well as arithmetic operations using mudu
  • More usage: covers more advanced features such as registering new conversions and creating custom units and dimensions

Prerequisites

To follow this tutorial and get the most out of it, it is assumed that readers have:

  • Basic understanding of the Python programming language
  • Secondary school (or equivalent) knowledge of dimensions and dimensional analysis

Basic Usage

Specifying units

To specify units, simply import the necessary dimensions and units, and create a dimension object.

from mudu import Length, METER, INCH
from mudu import Time, SECOND, HOUR
from mudu import Force, NEWTON, DYNE
from mudu import Pressure, PASCAL, mmHg

# create a fundamental quantity
length = Length(12, INCH)

t0 = Time(2, HOUR)

# create a derived quantity
force = Force(1, NEWTON)

pressure = Pressure(12, PASCAL)

Objects like Length, Mass, Time are called dimensions, while METER, INCH and NEWTON are called units. Note that dimensions are defined in title case, while units are defined in all caps. For a more comprehensive list of dimensions and units, check the API Reference on mudu.readthedocs.io.

Unit conversion

Unit conversion is done using the dimension's convert_to method.

length.convert_to(METER)

t0.convert_to(SECOND)

t0.convert_to(METER)    # does not make sense

# there is also support for conversion between some derived quantities
force.convert_to(DYNE)
pressure.convert_to(mmHg)

pressure.convert_to(NEWTON)     # definitely does not make sense

NOTE: Converting between units representing different dimensions raises a mudu.exceptions.DimensionError.

The scalar value, symbol, dimension and quantity (for derived quantities) can also be accessed, e.g.:

# for fundamental quantities
length.value    # -> 12
length.symbol   # -> in
length.dimension    # -> L

# for derived quantity
force.value # -> 1
force.quantity  # -> force
force.dimension # -> L*M/T**2
force.unit_type # -> N

NOTE: obj.dimension returns a sympy sympy.core child object that represents the dimension of the unit, and in the case of derived quantities, it performs a sort of dimensional analysis.

velocity = length/t0
velocity.dimension  # -> L/T

Operating with unit multiple prefixes

It is also possible to create units with multiples by specifying their multiple prefixes.

from mudu import Length, METER, INCH, Force, NEWTON, KILO, MILLI, OrderUnit

# creating units in their multiples
KILONEWTON = OrderUnit(KILO, NEWTON)
MILLIMETER = OrderUnit(MILLI, METER)
KILOINCH = OrderUnit(KILO, INCH)    # if it makes sense to you

l = Length(1000, MILLIMETER)
F = Force(20, KILONEWTON)

area = l * l

# you can also convert units with multiples
l_in_meter = l.convert_to(METER)    # very valid
new_area = l_in_meter * l_in_meter

pressure = F / new_area

OrderUnit is used to create units in their multiples, and supports the same operations as _UnitType. KILO and MILLI (and other multiple prefixes) are instances of _OrderType. See the API Reference for more information.

Arithmetic Operations

mudu objects also support arithmetic operations provided they are legal in the context provided. Illegal operations trigger exceptions. For example:

# legal arithmetic operations
total_length = length + length  # in inches

# adding a unit object to a scalar returns a scalar
small_length = total_length + 1     # same as total_length.value + 1
large_length = 5.3 + small_length  # same as 5.3 + small_length.value

The subtraction operator treats data just like the addition operator would.

small_length = Length(1, INCH)
total_length = length - small_length  # in inches

# subtracting a scalar from a unit object returns a scalar
smaller_length = total_length - 1     # same as total_length.value - 1

NOTE: Not all addition and subtraction operations are valid — some would cause errors because they are dimensionally incompatible. For example:

t = Time(12, SECOND)
l = Length(144, METER)

# an illegal arithmetic operation would look like
t_l = t + l     # adding time and length dimensions does not make sense

Adding time and length dimension objects does not make sense, so this operation raises a mudu.exceptions.DimensionError.

Multiplication and division operations follow all dimensional rules as well.

# unit multiplication and division operations
area = length ** 2  # result is a DerivedQuantity object
pressure = force / area # also a DerivedQuantity object

# and you can still check the following
pressure.value
pressure.quantity
pressure.dimension
pressure.symbol

# operations like these are also allowed
p0 = 3 * pressure
p_inv = 1 / pressure

NOTE: Multiplication and division operations between two or more _DimensionType objects return a DerivedQuantity object — it's really doing dimensional analysis under the hood. Where the arithmetic operation is between data of the same dimension but a different unit, the right-hand operand is implicitly converted to the same unit as the left operand.

length_in_m = Length(2, METER)

total_length = length + length_in_m # total_length is now in INCHes

l_sqr = length_in_m * length    # l_sqr is in METERs

By checking the type of length and force, their types are mudu.dimensions.Length and mudu.dimensions.Force respectively, but note that arithmetic between two dimension objects returns the generic DerivedQuantity type rather than a more specific subclass:

surface_tension = force * length
isinstance(surface_tension, mudu.dimensions.DerivedQuantity)    # True
isinstance(surface_tension, type(force))                        # False

So it is worth noting that every derived quantity is a child class of mudu.dimensions.DerivedQuantity, while every fundamental quantity is a child class of mudu.dimensions._DimensionType. Both classes inherit from _DimensionUnitBase.

Other operations such as int, float, round are also possible:

# same as int(length.value)
int(length)

# same as float(length.value)
float(length)

# round length.value to x decimal places, the unit is preserved
round(length*0.0122, 2) # round to 2 decimal places

# floor division is also possible, and correctly floors negative results
r = Length(12.23, METER)
r // 2  # -> 6.0

Length(-7, METER) // 2  # -> -4.0, not -3.0

Let's try something:

from mudu import Length, METER, Pressure, PSI, Force, NEWTON

length = Length(12, METER)
force = Force(112, NEWTON)
pressure = Pressure(12, PSI)

area = length * length
pressure_2 = force / area

pressure == pressure_2  # is False

pressure.dimension == pressure_2.dimension  # is True

That example is intended to show the idea of dimensional homogeneity. Quantity equality (==) always returns a plain bool, and quantities are hashable, so they can be used in sets and as dict keys.

More Usage

Dimension objects like Length, Time and Force have some built-in conversions defined via their class attribute, _conversion_standards — that's why it's possible to convert between units, provided the dimensions match. This isn't always the case for all dimensions or quantities (a freshly-defined custom quantity has none by default); to make a dimension convertible to a new unit, the new unit must:

  • first be defined against the dimension, via define_unit
  • then have its conversion registered against that dimension's base unit, via register_conversion
from mudu import Length, METER, define_unit, register_conversion, Linear

# define a new LENGTH unit
NEW_UNIT = define_unit(Length, name="new_unit", symbol="nu")

# register how it converts to Length's base unit (METER):
# base_value = value * scale
register_conversion(Length, NEW_UNIT, Linear(0.001))  # 1 new_unit = 0.001 m

l1 = Length(12, NEW_UNIT)
l2 = l1.convert_to(METER) + Length(4, METER)    # l2 is in METERs

For conversions that involve an offset rather than a pure scale factor (as with temperature scales), use Affine(scale, offset) instead of Linear(scale): base_value = value * scale + offset.

from mudu import Temperature, KELVIN, define_unit, register_conversion, Affine

MY_SCALE = define_unit(Temperature, name="my_scale", symbol="ms")
register_conversion(Temperature, MY_SCALE, Affine(scale=1.0, offset=100.0))

Once you've registered several custom units, mudu.audit_units() can scan your dimension's conversion table for duplicate unit names or malformed conversion entries — the same check mudu's own CI runs against its built-in tables:

from mudu import audit_units
from mudu.units import _LENGTH_CONVERSION_TABLE

problems = audit_units(_LENGTH_CONVERSION_TABLE)
assert problems == [], problems

To create a custom quantity or "dimension" that has no existing mudu dimension to attach to at all, simply inherit from _DimensionType or DerivedQuantity directly, and construct its units with _UnitType (this is the one case where using the private _UnitType class directly is still the correct, intended pattern — there's no existing table to register against).

from mudu.dimensions import DerivedQuantity
from mudu.base import _UnitType
from mudu import NEWTON, METER, SECOND

class Power(DerivedQuantity):
    _conversion_standards = None

    def __init__(self, value, unit_definition):
        super().__init__(value, unit_definition, quantity="power")

JOULES = _UnitType(
    _dimension=((NEWTON*METER)/SECOND)._dimension,
    _unit_name="joules",
    _unit_symbol="J",
    _quantity="power",
    _order=None,
)

power = Power(12, JOULES)

NOTE: As of this current version, some derived quantities have not been implemented; other quantities and units will be implemented as soon as possible (tracked in the project roadmap's SI-completeness backlog). A good practice is creating all custom-defined units in a separate file and then registering them against the relevant dimension in your main file, but the decision is up to you.

As a way of ending this tutorial, try the following on the Python REPL:

>>> from mudu import Pressure, PSI, METER, SECOND
>>>
>>> METER
m
>>>
>>> METER * METER
m^2
>>>
>>> PSI / METER
psi/m
>>>
>>> 3 * PSI
psi
>>>
>>> PSI / 3
psi
>>>
>>> PSI * 3
psi
>>>
>>> 4 / PSI
1/psi
>>>
>>> PSI + METER
Traceback (most recent call last):
  File "<python-input-18>", line 1, in <module>
    PSI + METER
    ~~~~^~~~~~~
TypeError: unsupported operand type(s) for +: '_UnitType' and '_UnitType'
>>>
>>> 1 / (PSI * METER)
1/(mpsi)

The idea behind the example above is to illustrate the way _UnitType objects can perform arithmetic operations independently of any quantity wrapping them.

I hope you now have a grasp of how to use mudu and its features, and find it useful and beneficial to your scientific computation projects.

Testing

pip install -e ".[test]"
pytest tests/test_suite.py -v

Contributing

Contributions are welcome — bug fixes, optimizations, documentation, and issue reports. See CONTRIBUTING.md for the workflow and coding guidelines.

Security

Found a vulnerability, or a conversion/dimension check that silently produces a wrong result? See SECURITY.md for how to report it responsibly rather than opening a public issue.

Changelog

See CHANGELOG.md for release history, including the 2.0.0 correctness and architecture overhaul.

Acknowledgments

Logo design by Odafe Megida (@Ddesigngeek).

License

MIT — see LICENSE.

Metadata

Release files for mudu 2.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for mudu 2.0.0
File Size Uploaded
mudu-2.0.0.tar.gz 42.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for mudu 2.0.0
File Interpreter ABI Platform
mudu-2.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 78.4 kB

Release files / mudu-2.0.0.tar.gz

Download URL mudu-2.0.0.tar.gz
Size 42.2 kB
Tags Source
SHA-256 checksum
How to use checksums
2a197186b515855dd65506d97f16634c8dcc5c020e5137f4a61aabee77417a6b
BLAKE2b-256 checksum
How to use checksums
d4fbd26038a5051eb88bd12196bfeafb6dfa2ed316bad5b8a38207b3627247db
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 29, 2026.

Transparency log

Release files / mudu-2.0.0-py3-none-any.whl

Download URL mudu-2.0.0-py3-none-any.whl
Size 36.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6ce86ec556555dad67df74dc0986179ce57bf7b9a19c456d1ca47293319ab547
BLAKE2b-256 checksum
How to use checksums
541c5c4357e95a6cf858a7b2b299556f282a5dc42ad43f66e4b43fb767b65bf1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 29, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

2.0.0 This release

2 release files

1.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page