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
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)
| File | Size | Uploaded | |
|---|---|---|---|
| base_typed_float-0.1.0.tar.gz | 21.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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}
|