Skip to content

Conversation

NoahStapp
Copy link
Contributor

@NoahStapp NoahStapp commented Aug 19, 2025

Context

We currently reject Python decimal.Decimal instances and require the user to manually convert to Decimal128.

This PR adds a new convert_decimal kwarg to CodecOptions that enables automatic conversion of decimal.Decimal to Decimal128 during BSON encoding.

Summary and example of changes

Current behavior and new behavior when convert_decimal=False (default)

>>> bson.encode({"d": decimal.Decimal('1.0')})
Traceback (most recent call last):
  File "<python-input-7>", line 1, in <module>
    encode({"d": decimal.Decimal('1.0')})
    ~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/shane/git/mongo-python-driver/bson/__init__.py", line 1053, in encode
    return _dict_to_bson(document, check_keys, codec_options)
bson.errors.InvalidDocument: Invalid document {'d': Decimal('1.0')} | cannot encode object: Decimal('1.0'), of type: <class 'decimal.Decimal'>

New behavior when convert_decimal=True

>>> bson.encode({"d": decimal.Decimal('1.0')}, codec_options=CodecOptions(convert_decimal=True))
b'\x18\x00\x00\x00\x13d\x00\n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>0\x00'

Testing

test/test_bson.py has a new test TestCodecOptions.test_convert_decimal to verify correctness.

A changelog entry will be added in this PR if we determine that this is the correct approach to move ahead with.

@NoahStapp NoahStapp requested a review from a team as a code owner August 19, 2025 14:50
@NoahStapp NoahStapp requested a review from ShaneHarvey August 19, 2025 14:55
@NoahStapp
Copy link
Contributor Author

NoahStapp commented Aug 19, 2025

Will update the test/test_custom_types.py tests to reflect the change later today.

@NoahStapp NoahStapp requested review from aclark4life and sleepyStick and removed request for ShaneHarvey August 19, 2025 15:29
Copy link
Contributor

@sleepyStick sleepyStick left a comment

Choose a reason for hiding this comment

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

Just a couple of minor comments

@NoahStapp NoahStapp requested a review from sleepyStick August 20, 2025 16:46
@@ -791,14 +812,15 @@ int convert_codec_options(PyObject* self, PyObject* options_obj, codec_options_t

options->unicode_decode_error_handler = NULL;

if (!PyArg_ParseTuple(options_obj, "ObbzOOb",
if (!PyArg_ParseTuple(options_obj, "ObbzOObb",
Copy link
Contributor

Choose a reason for hiding this comment

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

I stared at this for a while but then found: https://docs.python.org/3/c-api/arg.html. So, just confirming that's a format string for the args below.

Copy link
Contributor

Choose a reason for hiding this comment

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

lol i was sitting on this one too, googled it, found that same site, was still confused / overwhelemed by words so then i asked mongo-gpt HAHA

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup this is a format string.

@@ -1436,6 +1462,16 @@ static int _write_element_to_buffer(PyObject* self, buffer_t buffer,
in_fallback_call);
Py_DECREF(binary_value);
return result;
} else if (options->convert_decimal && _check_decimal(value)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

So after case 100 or so and if case 19 wasn't the case, auto-convert ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For consistency with how the rest of _write_element_to_buffer works, we check the converison edge cases such as this after the native BSON case statement.

Copy link
Contributor

@aclark4life aclark4life left a comment

Choose a reason for hiding this comment

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

LGTM!

@NoahStapp NoahStapp requested a review from blink1073 August 20, 2025 17:03
Copy link
Member

@ShaneHarvey ShaneHarvey left a comment

Choose a reason for hiding this comment

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

This PR doesn't add new functionality since users can already configure a CodecOptions that automatically encodes Decimal, for example:

class DecimalEncoder(TypeEncoder):
    @property
    def python_type(self):
        return Decimal

    def transform_python(self, value):
        return Decimal128(value)


DECIMAL_CODECOPTS = CodecOptions(type_registry=TypeRegistry([DecimalEncoder()]))

TypeRegistry can also be configured to auto-decode Decimal128 to decimal by implementing TypeDecoder.

Given this, is it necessary to add this convert_decimal flag?

converted = Decimal128(value)
return b"\x13" + name + converted.bid
else:
raise InvalidDocument("decimal.Decimal must be converted to bson.decimal128.Decimal128.")
Copy link
Member

Choose a reason for hiding this comment

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

Suggest mentioning the convert_decimal option in this error message.

@NoahStapp
Copy link
Contributor Author

NoahStapp commented Aug 20, 2025

This PR doesn't add new functionality since users can already configure a CodecOptions that automatically encodes Decimal, for example:

class DecimalEncoder(TypeEncoder):
    @property
    def python_type(self):
        return Decimal

    def transform_python(self, value):
        return Decimal128(value)


DECIMAL_CODECOPTS = CodecOptions(type_registry=TypeRegistry([DecimalEncoder()]))

TypeRegistry can also be configured to auto-decode Decimal128 to decimal by implementing TypeDecoder.

Given this, is it necessary to add this convert_decimal flag?

The convert_decimal flag is significantly less friction for users than configuring their own CodecOptions with an Encoder. Especially since practically every custom TypeEncoder to do so will be identical. If that's the case, we should do the conversion automatically ourselves.

I'm fine to make this auto-conversion the default instead of adding a flag, but then we would explicitly disallow users from making their own custom Encoders for decimal.Decimal.

@NoahStapp
Copy link
Contributor Author

The Decimal128 specification explicitly does not allow this kind of auto-conversion. Closed.

@NoahStapp NoahStapp closed this Aug 20, 2025
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.

5 participants