Metadata-Version: 2.4
Name: base-typed-float
Version: 0.1.0
Summary: Strict finite branded and constrained float base classes with exact runtime subtype preservation.
Author-email: Eldeniz Guseinli <eldenizfamilyanskicode@gmail.com>
License-Expression: MIT
Project-URL: Changelog, https://github.com/eldenizfamilyanskicode/base-typed-float/blob/main/CHANGELOG.md
Project-URL: Homepage, https://github.com/eldenizfamilyanskicode/base-typed-float
Project-URL: Repository, https://github.com/eldenizfamilyanskicode/base-typed-float
Project-URL: Issues, https://github.com/eldenizfamilyanskicode/base-typed-float/issues
Keywords: typing,typed-float,float,value-object,pydantic,domain-model
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: pydantic
Requires-Dist: pydantic<3,>=2.6; extra == "pydantic"
Provides-Extra: test
Requires-Dist: pytest>=8.0; extra == "test"
Requires-Dist: pytest-cov>=5.0; extra == "test"
Requires-Dist: pydantic<3,>=2.6; extra == "test"
Provides-Extra: lint
Requires-Dist: ruff>=0.5; extra == "lint"
Provides-Extra: typecheck
Requires-Dist: mypy>=1.10; extra == "typecheck"
Requires-Dist: pyright>=1.1; extra == "typecheck"
Requires-Dist: pydantic<3,>=2.6; extra == "typecheck"
Requires-Dist: typing-extensions>=4.10; extra == "typecheck"
Provides-Extra: build
Requires-Dist: build>=1.2; extra == "build"
Requires-Dist: twine>=5.1; extra == "build"
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == "dev"
Requires-Dist: twine>=5.1; extra == "dev"
Requires-Dist: mypy>=1.10; extra == "dev"
Requires-Dist: pyright>=1.1; extra == "dev"
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-cov>=5.0; extra == "dev"
Requires-Dist: ruff>=0.5; extra == "dev"
Requires-Dist: pydantic<3,>=2.6; extra == "dev"
Requires-Dist: typing-extensions>=4.10; extra == "dev"
Dynamic: license-file

# 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

```bash
pip install base-typed-float
```

With Pydantic v2 support:

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

## Basic usage

```python
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:

```python
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:

```python
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:

```python
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

```python
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

```bash
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
