Skip to main content

Strict finite branded and constrained float base classes with exact runtime subtype preservation.

Project description

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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

base_typed_float-0.1.0.tar.gz (21.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

base_typed_float-0.1.0-py3-none-any.whl (12.0 kB view details)

Uploaded Python 3

File details

Details for the file base_typed_float-0.1.0.tar.gz.

File metadata

  • Download URL: base_typed_float-0.1.0.tar.gz
  • Upload date:
  • Size: 21.3 kB
  • Tags: Source
  • Uploaded using 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}

File hashes

Hashes for base_typed_float-0.1.0.tar.gz
Algorithm Hash digest
SHA256 41734e92d593fe06428d0ece6b36adf66920ac4b5098bd60a18383f7943be565
MD5 987cf704e13fc39d8327f44dcda1e440
BLAKE2b-256 2f044ba4bf62aba2c5e0d85c4a4ff7924d1057a75febec9e9d1ad4811ada1e7a

See more details on using hashes here.

File details

Details for the file base_typed_float-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: base_typed_float-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 12.0 kB
  • Tags: Python 3
  • Uploaded using 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}

File hashes

Hashes for base_typed_float-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 65d907c4942557788c23b9ec00fd9be8d79a383ac89903e391cd6fb54f14411c
MD5 5b9a329e939bc31e550dbfdd53a01b22
BLAKE2b-256 5fe52f5b04703e5b7b5536776e4f2835e640973cda5576f35cf66e0066e70085

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page