Skip to content

[HZ-5510] Introduced number types and removed the default_int_type config - #837

Open
yuce wants to merge 4 commits into
masterfrom
number-types
Open

[HZ-5510] Introduced number types and removed the default_int_type config#837
yuce wants to merge 4 commits into
masterfrom
number-types

Conversation

@yuce

@yuce yuce commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Hazelcast has a number of default serializers for numeric types, modeled after the corresponding Java types: signed 8, 16, 32, 64 bit integers, 32 and 64 bit floating point numbers. Go, C++ and .Net clients have corresponding native types, so they are able to serialize numeric values with the intended type. Python and Node.js clients have a different story though.

Python has a single unbounded integer type, and a single floating point type. When a user saves a number using the Python client, it is not immediately apparent which data type is used for storage. Consider the following example:

client = await HazelcastClient.create_and_start( ... )
m = await client.get_map("my-map")
await m.set("key", 123)

A configuration based mechanism was used as a workaround to solve this problem, and ensure a number is stored with the intended type. Python Client has the default_int_type configuration which sets the integer type to be used when storing an integer to the cluster. It must be set during client creation, and cannot be changed later:

client = await HazelcastClient.create_and_start(default_int_type=IntType.SHORT)
m = await client.get_map("my-map")
await m.set("key", 123)

The obvious problem with this approach is, the integer/number setting is global to a client, and can be set only once. If an application uses different number types, there is no way of storing values with different types using the client.

As I’ve shown, serializing number types is problematic using the Python Client. How about deserialization? The values are deserialized to the expected type: int so there’s not a problem, unless the user wants to put the same value back to the cluster. That problem is out of scope for this proposal, and a possible solution will be explained in a future proposal.

This problem can be easily solved by introducing missing number types. For example, the hazelcast.Int16 class can be used to serialize a value as a 16 bit signed integer:

from hazelcast import Int16
m = await client.get_map("my-map")
await m.set("key", Int16(123))

That allows us to remove the default_int_type from configuration and related code. We can keep serializing integers by default as 32 bit signed integers for convenience. So if the user never sets default_int_type other than the default, their code still works as expected.

await m.set("key", 123)  # serialized as 32 bit signed integer
await m.set("key", Int16(123))  # serialized as 16 bit signed integer

This PR:

  • Removes default_int_type configuration and related code.
  • Introduces the following number classes: Int8, Int16, Int32, Int64, BigInt, Float32, Float64
  • Unwrapped integers are stored as 32 bit integers (keeps the previous behavior).
  • Unwrapped floats are stored as 64 bit floats (keeps the previous behavior).

NOTE: Do not mind the link checker failure, they are due to AI-protection mechanism.

@yuce yuce changed the title Introduced number types and removed the default_int_type config [HZ-5510] Introduced number types and removed the default_int_type config Aug 24, 2026
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.64045% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.16%. Comparing base (ba315d5) to head (9b459ed).

Files with missing lines Patch % Lines
hazelcast/number_types.py 84.05% 11 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #837      +/-   ##
==========================================
- Coverage   94.21%   94.16%   -0.05%     
==========================================
  Files         413      414       +1     
  Lines       27646    27688      +42     
==========================================
+ Hits        26046    26073      +27     
- Misses       1600     1615      +15     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@yuce
yuce requested a review from ihsandemir August 24, 2026 14:44

@ihsandemir ihsandemir left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about reader side? Why dont we convert to Int8, Int16, Int64, etc, on the read side?

Comment thread hazelcast/number_types.py
return str(self.value)


class Float64:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should it be named to Double to be in sync with Java naming?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double is mostly just meaningful to Java developers.
Float64 communicates that this type is a 64bit float.
Also there is some inconsistency about this even in the Java client, e.g., compact serializer has the writeFloat64 method.


def write(self, out, obj):
out.write_short(obj)
out.write_short(int(obj))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
out.write_short(int(obj))
out.write_short(Int16(obj))

and similar for the other int changes below.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be incorrect. We must convert Int16 to Python's int type, since that's what write_short expects.

@ihsandemir ihsandemir left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice cleanup, explicit number types read much better than the old default_int_type config.

I ran the unit tests, mypy and black on the branch and they all pass, and I did not find a functional bug in the change itself. Most comments below are about the new number_types classes: they have no __eq__ / __hash__ and do not subclass int / float, which makes them awkward to use in normal code. There is also a docs gap, and one test that quietly stopped covering three serializers.

Comment thread hazelcast/number_types.py
@@ -0,0 +1,162 @@
from typing import Self

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self is imported but never used anywhere in the file. Looks like a leftover and can be dropped.

Comment thread hazelcast/number_types.py
__all__ = "Int8", "Int16", "Int32", "Int64", "Float32", "Float64", "BigInt"


class Int8:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These types have no __eq__ / __hash__, so they compare by identity:

Int32(1) == Int32(1)  # False
Int32(1) == 1         # False

This also affects map.get_all(): it puts the keys in a dict, so get_all([Int32(1), Int32(1)]) cannot dedupe and sends the same key to the member twice.

A simpler fix for the whole file: let Int8..Int64 subclass int, and Float32/Float64 subclass float. Then equality, hashing, arithmetic, indexing and Map[str, int] type hints all work for free. Today Int32(5) + 1 and sum([Int32(1), Int32(2)]) raise TypeError.

Dispatch keeps working, because lookup_default_serializer matches the exact type (obj_type is int), which is the same reason bool works today.

Comment thread hazelcast/number_types.py
MIN_VALUE = MIN_BYTE
MAX_VALUE = MAX_BYTE

def __init__(self, value: int):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The range is checked but the type is not, so a wrong value is only caught much later:

Int8(1.5)   # accepted here, fails in to_data with "__int__ returned non-int (type float)"
Int8("3")   # raises TypeError from the comparison, not the intended ValueError

An isinstance(value, int) check before the range check would report it at the call site.

Comment thread hazelcast/number_types.py
Corresponds to Java ``float``.
"""

def __init__(self, value: float | int):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No range check here, unlike the Int types. Float32(1e300) is accepted and then fails at write time with float too large to pack with f format.

A range check would be good. It may also be worth a docstring note that precision is lost: Float32(1.1) reads back as 1.100000023841858. That is expected for a 32-bit float, but it surprises people.

Comment thread hazelcast/number_types.py


class Float64:
"""Float32 represents a 64-bit floating point number

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: should be Float64 represents a 64-bit floating point number. This line shows up in the generated API docs.

obj = REFERENCE_OBJECTS[name]
# bool is an instance of int, so need to exclude that specifically --YT
if not isinstance(deserialized, bool) and isinstance(deserialized, int):
obj = int(obj)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does nothing. REFERENCE_OBJECTS holds plain int values for Byte/Short/Integer/Long/BigInteger, so int(obj) never converts anything.

Looks like a leftover from an earlier version where the reference values were wrapped.

ss = self._create_serialization_service(is_big_endian)
obj = REFERENCE_OBJECTS[name]
data = ss.to_data(obj)
if name == "BigInteger":

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only BigInteger is wrapped here, while test_serialize above also wraps Byte, Short and Long. So for those three the reference object stays a plain int and is serialized as Integer.

The test still passes, because REFERENCE_OBJECTS["Long"] is -50992225 and fits into an int32. But that means ByteSerializer, ShortSerializer and LongSerializer round-trips are no longer tested at all, and a real regression in them would go unnoticed.

Could you wrap them here the same way test_serialize does?

from hazelcast import HazelcastClient, Int8, Int16, Int32, Int64, Float32, Float64
from hazelcast.core import HazelcastJsonValue
from hazelcast.number_types import BigInt
from hazelcast.serialization import MAX_BYTE, MAX_SHORT, MAX_INT, MAX_LONG

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are unused now that test_variable_integer is gone.

Minor: BigInt is imported from hazelcast.number_types while the others come from hazelcast. BigInt is in __all__, so it can come from hazelcast too.

@@ -0,0 +1,15 @@
import asyncio

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few small things in this example:

  • The client is never shut down and there is no if __name__ == "__main__": guard, so the process does not exit cleanly. Other examples, e.g. examples/asyncio/map_basic_example.py, do both.
  • value_i8 is read from a key "i8" that was written as Int32(10). Int32 is also the one type that needs no wrapper, since a plain int already maps to it, so Int8 would show the feature better.
  • map shadows the builtin.

Comment thread docs/config.rst
.. autoclass:: FlakeIdGeneratorConfig
.. autoclass:: ReliableTopicConfig
.. autoclass:: IntType
.. autoclass:: EvictionPolicy

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IntType is removed here, but nothing is added for the new types, and docs/serialization.rst still describes the old behaviour: line 23 maps int to Byte/Short/Integer/Long/BigInteger, and line 37 tells users to configure this with default_int_type. That argument is removed by this PR, so following the docs now raises InvalidConfigurationError: Unrecognized config option: default_int_type.

Could Int8..BigInt get an entry here and a short section in serialization.rst?

@yuce

yuce commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

What about reader side? Why dont we convert to Int8, Int16, Int64, etc, on the read side?

Python has a single integer type.
If we return Int8, etc, the user cannot use them directly, and must convert them to int, which is not convenient.
The only use of returning Int8 etc would be putting it back to the map (e.g., for implementing map copy).
But in our current APIs most of the methods eagerly deserialize, so we need a lazy deserialization mechanism to accomplish that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants