Skip to main content

base-typed-float

base_typed_float provides small base classes for finite, domain-specific floating-point values that remain real float objects at runtime.

Contract

  • only an exact built-in float is accepted by direct construction
  • int, bool, str, Decimal, Fraction, complex values, foreign numeric scalars, and float subclasses are rejected
  • NaN, positive infinity, and negative infinity are always rejected
  • no arithmetic method is overridden
  • normal arithmetic returns the native result chosen by Python
  • Pydantic v2 support is optional and preserves the exact declared subtype
  • constrained types support only deterministic range constraints: gt, ge, lt, and le

Installation

pip install base-typed-float

With Pydantic v2 support:

pip install "base-typed-float[pydantic]"

Basic usage

from base_typed_float import BaseTypedFloat


class TemperatureCelsius(BaseTypedFloat):
    pass


temperature = TemperatureCelsius(21.5)

assert type(temperature) is TemperatureCelsius
assert isinstance(temperature, float)
assert repr(temperature) == "TemperatureCelsius(21.5)"

adjusted = temperature + 0.5
assert adjusted == 22.0
assert type(adjusted) is float

The following values are rejected rather than coerced:

from decimal import Decimal

from base_typed_float import (
    BaseTypedFloatInvalidInputValueError,
    BaseTypedFloatNonFiniteValueError,
)


for invalid_value in (1, True, "1.0", Decimal("1.0")):
    try:
        TemperatureCelsius(invalid_value)  # type: ignore[arg-type]
    except BaseTypedFloatInvalidInputValueError:
        pass

for non_finite_value in (float("nan"), float("inf"), float("-inf")):
    try:
        TemperatureCelsius(non_finite_value)
    except BaseTypedFloatNonFiniteValueError:
        pass

Direct construction intentionally rejects an already branded float as well. Domain values are not silently converted from one meaning to another.

Constrained floats

Use BaseConstrainedTypedFloat when a finite range belongs to the named type:

from base_typed_float import BaseConstrainedTypedFloat


class CompletionRatio(BaseConstrainedTypedFloat):
    ge = 0.0
    le = 1.0


ratio = CompletionRatio(0.75)
assert type(ratio) is CompletionRatio

Constraint declarations must be exact built-in finite floats. For example, ge = 0 is rejected; write ge = 0.0.

Property Meaning
gt value must be strictly greater than the bound
ge value must be greater than or equal to the bound
lt value must be strictly less than the bound
le value must be less than or equal to the bound

Invalid and empty ranges fail while the class is created. Empty-domain checks use adjacent representable IEEE-754 values, not an idealized real-number line. For example, no finite float exists strictly between a value and its immediate successor from math.nextafter.

Constraint declarations are sealed after class creation because Pydantic caches schemas. Mutation is detected on construction, validation, serialization, and JSON Schema generation. A child type may only narrow the domain of its single constrained parent.

Multiple constrained parents are rejected. A combined domain meaning must be a new independently declared type:

class PositiveRatio(BaseConstrainedTypedFloat):
    gt = 0.0
    le = 1.0


class NegativeRatio(BaseConstrainedTypedFloat):
    ge = -1.0
    lt = 0.0


# Wrong: one value type must not acquire two domain meanings.
# class NonZeroRatio(PositiveRatio, NegativeRatio):
#     pass


class SignedRatio(BaseConstrainedTypedFloat):
    ge = -1.0
    le = 1.0

Why there is no multiple_of

Binary floating-point does not give a universal, unsurprising meaning to multiple_of=0.1. Exact remainder checks reject familiar values such as 0.3, while tolerance-based checks introduce an arbitrary policy. This package does not guess. Use a decimal domain type when exact decimal steps are required.

Native runtime semantics

The package does not override addition, subtraction, multiplication, division, power, rounding, comparison, equality, or hashing. Consequently:

  • arithmetic results are ordinary Python numeric values
  • DomainFloat(1.0) == 1.0 == 1 follows normal Python behavior
  • matching hashes can share dictionary keys
  • -0.0 is accepted and follows normal IEEE-754/Python equality semantics

The branded subtype marks a validated boundary value; it is not a closed arithmetic algebra.

Pydantic v2

from pydantic import BaseModel


class Measurement(BaseModel):
    ratio: CompletionRatio


measurement = Measurement.model_validate({"ratio": 0.75})

assert type(measurement.ratio) is CompletionRatio
assert measurement.model_dump() == {"ratio": 0.75}
assert measurement.model_dump_json() == '{"ratio":0.75}'

Pydantic's own StrictFloat accepts integers and converts them to floats. This package validates the raw Python/JSON input first, so 1 is rejected while 1.0 is accepted. Passing an instance of the exact field type is idempotently accepted; instances of other branded float types are rejected.

JSON Schema exposes the range constraints as a JSON number. JSON Schema cannot exactly describe Python's distinction between parsed 1 and 1.0, so the strict runtime input rule remains part of this documented contract.

Pickle and JSON

Pickle roundtrips reconstruct the exact subtype through its validated constructor. Standard json.dumps serializes instances as ordinary JSON numbers.

Development

uv sync --extra dev
uv run ruff format --check .
uv run ruff check .
uv run mypy
uv run pyright
uv run pytest
uv build
uv run twine check dist/*

The test suite requires 100% branch coverage.

Compatibility

  • CPython 3.10+
  • optional Pydantic v2 integration
  • no runtime dependencies for direct construction

License

MIT

Metadata

Release files for base-typed-float 0.1.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 base-typed-float 0.1.0
File Size Uploaded
base_typed_float-0.1.0.tar.gz 21.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for base-typed-float 0.1.0
File Interpreter ABI Platform
base_typed_float-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 33.3 kB

Release files / base_typed_float-0.1.0.tar.gz

Download URL base_typed_float-0.1.0.tar.gz
Size 21.3 kB
Tags Source
SHA-256 checksum
How to use checksums
41734e92d593fe06428d0ece6b36adf66920ac4b5098bd60a18383f7943be565
BLAKE2b-256 checksum
How to use checksums
2f044ba4bf62aba2c5e0d85c4a4ff7924d1057a75febec9e9d1ad4811ada1e7a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.13 {"installer":{"name":"uv","version":"0.11.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / base_typed_float-0.1.0-py3-none-any.whl

Download URL base_typed_float-0.1.0-py3-none-any.whl
Size 12.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
65d907c4942557788c23b9ec00fd9be8d79a383ac89903e391cd6fb54f14411c
BLAKE2b-256 checksum
How to use checksums
5fe52f5b04703e5b7b5536776e4f2835e640973cda5576f35cf66e0066e70085
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.13 {"installer":{"name":"uv","version":"0.11.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

This release

0.1.0 This release

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