Skip to main content

Strict typed integer base class with exact runtime subtype preservation and optional Pydantic v2 support.

Project description

base-typed-int

base_typed_int is a small Python library for building domain-specific integer types that remain real int objects at runtime.

Strict typed integer base class with exact runtime subtype preservation and optional Pydantic v2 support.

Why

BaseTypedInt helps model domain integers as explicit runtime types without losing normal int behavior.

Example use cases:

  • UserAge
  • RetryCount
  • OrderPosition
  • PriorityLevel
  • ShardNumber

This library is designed for cases where plain int is too generic, but a full wrapper object would add unnecessary ceremony and runtime overhead.

Design goals

  • exact runtime subtype is preserved
  • behaves like plain int in normal arithmetic operations
  • arithmetic operations usually return plain int
  • subtype is preserved in containers, attributes, pickle, and Pydantic model fields
  • Pydantic serialization exports plain integers
  • strict input validation
  • bool is explicitly rejected
  • no extra public API beyond the integer value itself

Installation

Core

pip install base-typed-int

With Pydantic v2 support

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

Development

pip install "base-typed-int[dev]"

Quick start

from base_typed_int import BaseTypedInt

class UserAge(BaseTypedInt):
    pass

class Account:
    def __init__(self, user_age: UserAge) -> None:
        self.user_age: UserAge = user_age


user_age: UserAge = UserAge(18)

assert user_age == 18
assert isinstance(user_age, int)
assert isinstance(user_age, UserAge)
assert type(user_age) is UserAge
assert repr(user_age) == "UserAge(18)"

account: Account = Account(user_age=user_age)
assert account.user_age is user_age

Constructor behavior

The constructor is typed as accepting int, while still keeping a runtime validation guard for invalid non-integer inputs crossing dynamic boundaries.

from base_typed_int import BaseTypedInt

class RetryCount(BaseTypedInt):
    pass


value_from_int: RetryCount = RetryCount(3)
value_from_typed_int: RetryCount = RetryCount(RetryCount(3))

Invalid input raises BaseTypedIntInvalidInputValueError.

from base_typed_int import (
    BaseTypedInt,
    BaseTypedIntInvalidInputValueError,
)

class RetryCount(BaseTypedInt):
    pass

try:
    RetryCount("3")
except BaseTypedIntInvalidInputValueError:
    pass

try:
    RetryCount(True)
except BaseTypedIntInvalidInputValueError:
    pass

Why bool is rejected

In Python, bool is a subclass of int.

assert isinstance(True, int)
assert int(True) == 1

That behavior is useful in Python internals, but usually unsafe for domain modeling. A domain integer such as RetryCount, UserAge, or ShardNumber should not silently accept True or False.

For that reason, BaseTypedInt explicitly rejects bool even though bool is technically an int subtype.

Runtime behavior

Normal arithmetic keeps standard Python int semantics.

from base_typed_int import BaseTypedInt

class UserAge(BaseTypedInt):
    pass


user_age: UserAge = UserAge(18)

incremented_value: int = user_age + 1
multiplied_value: int = user_age * 2
subtracted_value: int = user_age - 3

assert incremented_value == 19
assert multiplied_value == 36
assert subtracted_value == 15

assert type(incremented_value) is int
assert type(multiplied_value) is int
assert type(subtracted_value) is int

This is intentional. The typed subtype marks the boundary value itself, while regular numeric operations stay native and unsurprising.

Containers and attributes

The exact subtype instance is preserved when stored and retrieved.

from base_typed_int import BaseTypedInt

class UserAge(BaseTypedInt):
    pass


class Account:
    def __init__(self, user_age: UserAge) -> None:
        self.user_age: UserAge = user_age


source_user_age: UserAge = UserAge(18)

user_age_list: list[UserAge] = [source_user_age]
user_age_by_field_name: dict[str, UserAge] = {
    "user_age": source_user_age,
}
values_by_user_age: dict[int, str] = {
    source_user_age: "present",
}
account: Account = Account(user_age=source_user_age)

assert user_age_list[0] is source_user_age
assert user_age_by_field_name["user_age"] is source_user_age
assert account.user_age is source_user_age
assert values_by_user_age[source_user_age] == "present"
assert values_by_user_age[18] == "present"
assert type(tuple(values_by_user_age.keys())[0]) is UserAge

Pickle support

Pickle roundtrip preserves the exact subtype.

import pickle

from base_typed_int import BaseTypedInt

class RetryCount(BaseTypedInt):
    pass


source_value: RetryCount = RetryCount(7)
serialized_value: bytes = pickle.dumps(source_value)
restored_value: object = pickle.loads(serialized_value)

assert restored_value == 7
assert isinstance(restored_value, RetryCount)
assert type(restored_value) is RetryCount

JSON behavior

Since BaseTypedInt inherits from int, standard JSON serialization naturally produces plain JSON numbers.

import json

from base_typed_int import BaseTypedInt

class RetryCount(BaseTypedInt):
    pass


value: RetryCount = RetryCount(7)
serialized_value: str = json.dumps(value)
restored_value: object = json.loads(serialized_value)

assert serialized_value == "7"
assert restored_value == 7
assert type(restored_value) is int

Pydantic v2 support

BaseTypedInt integrates with Pydantic v2 through __get_pydantic_core_schema__.

Validation rules:

  • accepts only strict integer input
  • rejects bool
  • rejects strings and other non-integer values
  • reconstructs the exact runtime subtype

Serialization rules:

  • model_dump() returns plain integers
  • model_dump_json() returns JSON numbers

Example

from pydantic import BaseModel

from base_typed_int import BaseTypedInt

class RetryCount(BaseTypedInt):
    pass


class MetricsModel(BaseModel):
    primary_retry_count: RetryCount
    backup_retry_count: RetryCount


metrics_model: MetricsModel = MetricsModel.model_validate(
    {
        "primary_retry_count": 5,
        "backup_retry_count": 8,
    }
)

assert type(metrics_model.primary_retry_count) is RetryCount
assert type(metrics_model.backup_retry_count) is RetryCount

python_dump: dict[str, object] = metrics_model.model_dump()
json_dump: str = metrics_model.model_dump_json()

assert python_dump == {
    "primary_retry_count": 5,
    "backup_retry_count": 8,
}
assert json_dump == '{"primary_retry_count":5,"backup_retry_count":8}'

Roundtrip from exported payload

source_model: MetricsModel = MetricsModel.model_validate(
    {
        "primary_retry_count": 5,
        "backup_retry_count": 8,
    }
)

python_dump: dict[str, object] = source_model.model_dump()
restored_model: MetricsModel = MetricsModel.model_validate(python_dump)

assert type(restored_model.primary_retry_count) is RetryCount
assert type(restored_model.backup_retry_count) is RetryCount

Error types

from base_typed_int import (
    BaseTypedIntError,
    BaseTypedIntInvalidInputValueError,
    BaseTypedIntInvariantViolationError,
)
  • BaseTypedIntError — root exception for library errors
  • BaseTypedIntInvalidInputValueError — invalid caller input
  • BaseTypedIntInvariantViolationError — internal invariant or integration failure

Testing

pytest

With coverage:

pytest --cov=base_typed_int --cov-report=term-missing

Type checking

mypy src tests
pyright

Linting

ruff check .
ruff format .

Build

python -m build

Package structure

src/
  base_typed_int/
    __init__.py
    _base_typed_int.py
    _exceptions.py
    py.typed

Compatibility

  • Python 3.10+
  • CPython
  • Pydantic v2 support is optional

Notes

BaseTypedInt is intentionally minimal. It is not a numeric validation framework and does not enforce domain-specific constraints such as non-negative values, ranges, or upper bounds.

Those constraints should be modeled in your own subclasses or service-layer validation where appropriate.

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_int-0.1.1.tar.gz (12.0 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_int-0.1.1-py3-none-any.whl (7.1 kB view details)

Uploaded Python 3

File details

Details for the file base_typed_int-0.1.1.tar.gz.

File metadata

  • Download URL: base_typed_int-0.1.1.tar.gz
  • Upload date:
  • Size: 12.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for base_typed_int-0.1.1.tar.gz
Algorithm Hash digest
SHA256 860b51d05e0fbac112698819e373405e9653f81b95dd031337b01a59eade5949
MD5 b47b5bddd2eb748eb52699bb1165eaf3
BLAKE2b-256 da374f2b1fc49ada99782b50333114e2c127f28e35990a552353cfe44c1d1e17

See more details on using hashes here.

File details

Details for the file base_typed_int-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: base_typed_int-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 7.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for base_typed_int-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 47614e5ee198be348c402d1ee9ec3a0972fe62cd6c97b52f6f0dd830b3a691ce
MD5 ea477325d4bc653f89ab712c8e2b7b85
BLAKE2b-256 c4a82a5b534899452410e6cb3fbd195b0c88563f366462413f3f428d33f88e58

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