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
floatis accepted by direct construction int,bool,str,Decimal,Fraction, complex values, foreign numeric scalars, andfloatsubclasses are rejectedNaN, 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, andle
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 == 1follows normal Python behavior- matching hashes can share dictionary keys
-0.0is 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
41734e92d593fe06428d0ece6b36adf66920ac4b5098bd60a18383f7943be565
|
|
| MD5 |
987cf704e13fc39d8327f44dcda1e440
|
|
| BLAKE2b-256 |
2f044ba4bf62aba2c5e0d85c4a4ff7924d1057a75febec9e9d1ad4811ada1e7a
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
65d907c4942557788c23b9ec00fd9be8d79a383ac89903e391cd6fb54f14411c
|
|
| MD5 |
5b9a329e939bc31e550dbfdd53a01b22
|
|
| BLAKE2b-256 |
5fe52f5b04703e5b7b5536776e4f2835e640973cda5576f35cf66e0066e70085
|