Hi,
Is there any strong reason why using a DynamoDB's Table resource will convert the number type "N" to a Decimal() object?
Shouldn't it try looking up the right python type, such as int or float or long?
I am trying to unpack a record's data (a mapping) into a function call, specifically: next_execution = now() + datetime.timedelta(**dynamo_record['frequency']) but datetime.timedelta will not accept the Decimal object into its arguments, although it does accept long and float.
TypeError: unsupported type for timedelta days component: Decimal
This was one of the lessons learned from boto. If I remember correctly, there were issues with round tripping float values and that the built in float() type could not handle the 38 digits of precision supported in dynamodb's numeric types. This would result in not being able to delete items in the table:
>>> d = Decimal('1234567890123.12345678901234567890')
>>> d
Decimal('1234567890123.12345678901234567890')
>>> float(d)
1234567890123.1235
More background info:
https://github.com/boto/boto/issues/873
PR:
https://github.com/boto/boto/pull/1183
Perhaps we could support using ints() if there's no floating point in the number. Would need to investigate what the impact of that would be. Would that help in your scenario?
After a trip to python's doc on floating point limitations, I can see where this came from. It's a shame that the standard library doesn't support Decimal() in place of a float.
To answer your question specifically, it will not help my scenario. The timedelta supports floats, it's perfectly valid to do a timedelta(hours=1.5). I think it would only add to the confusion if boto3 would change between int and Decimal on a per-record basis as its default behavior.
Perhaps a small option somewhere to tell boto we don't really care about the added precision, so it can return floats and ints across the board?
FWIW, I use this little function to recurse into Python objects returned by the boto3 DynamoDB resource layer and convert any Decimal values to int or float. It is by no means foolproof and doesn't in any way solve the problem of lack of precision in Python's float type but it solves my problem which is mainly to turn the data into something that can be returned to API Gateway via a Python Lambda function.
def replace_decimals(obj):
if isinstance(obj, list):
for i in xrange(len(obj)):
obj[i] = replace_decimals(obj[i])
return obj
elif isinstance(obj, dict):
for k in obj.iterkeys():
obj[k] = replace_decimals(obj[k])
return obj
elif isinstance(obj, decimal.Decimal):
if obj % 1 == 0:
return int(obj)
else:
return float(obj)
else:
return obj
Thanks for the code @garnaat, that certainly works.
This issue is a big PITA.
Would adding some sort of use_decimal=False option to the config object when creating clients/resources be helpful?
Of course that would be useful :)
Ok, let's mark this as a feature request. I'll update the title.
:+1: for this feature
This particular problem likes to creep into my code in the most unusual places. Converting from Decimal to int/float is a thing, but it seems boto won't take my python floats anymore (did it take them before? i'm not sure) so I created the following function to prepare all of my data before sending it to dynamodb (kinda like @garnaat's method, but the other way around):
def _sanitize(data):
""" Sanitizes an object so it can be updated to dynamodb (recursive) """
if not data and isinstance(data, (basestring, Set)):
new_data = None # empty strings/sets are forbidden by dynamodb
elif isinstance(data, (basestring, bool)):
new_data = data # important to handle these one before sequence and int!
elif isinstance(data, Mapping):
new_data = {key: _sanitize(data[key]) for key in data}
elif isinstance(data, Sequence):
new_data = [_sanitize(item) for item in data]
elif isinstance(data, Set):
new_data = {_sanitize(item) for item in data}
elif isinstance(data, (float, int, long, complex)):
new_data = Decimal(data)
else:
new_data = data
return new_data
+1 for this
+1 for this feature request, I've now run into a similar problem to the one @jonapich experienced.
+1 for this feature request
+1
+1
any update on this ?
+1
+1
+1
+1
+1
+1
+1
+1
Seriously, this is not okay. Using python3.6 I can store math.floor(time.time()) in DynamoDB. Using python2.7 I cannot. A database is expected to be able to receive numbers. That's pretty basic.
+1
Just used the code by @garnaat and updated it to Python 3.6:
def replace_decimals(obj):
if isinstance(obj, list):
for i in range(len(obj)):
obj[i] = replace_decimals(obj[i])
return obj
elif isinstance(obj, dict):
for k, v in obj.items():
obj[k] = replace_decimals(v)
return obj
elif isinstance(obj, decimal.Decimal):
if obj % 1 == 0:
return int(obj)
else:
return float(obj)
else:
return obj
+1
+1
The code below is for saving to DynamoDB; use @flomotlik's code in https://github.com/boto/boto3/issues/369#issuecomment-302137290 to load floats from DynamoDB.
To allow rounding and inexact values and still prevent over/underflow and clamping, I'd recommend using a decimal.Context such as the one in boto3/dynamodb/types.py but drop the decimal.Inexact and decimal.Clamped traps. I'd also use numeric for the type check in the sanitizer instead of just checking for Decimal or float. The following serializer should be a bit more robust:
from collections.abc import Iterable, Mapping, ByteString, Set
import numbers
import decimal
context = decimal.Context(
Emin=-128, Emax=126, rounding=None, prec=38,
traps=[decimal.Clamped, decimal.Overflow, decimal.Underflow]
)
def dump_to_dynamodb(item):
# don't catch str/bytes with Iterable check below;
# don't catch bool with numbers.Number
if isinstance(item, (str, ByteString, bool)):
return item
# ignore inexact, rounding errors
if isinstance(item, numbers.Number):
return context.create_decimal(item)
# mappings are also Iterable
elif isinstance(item, Mapping):
return {
key: dump_to_dynamodb(value)
for key, value in item.values()
}
# boto3's dynamodb.TypeSerializer checks isinstance(o, Set)
# so we can't handle this as a list
elif isinstance(item, Set):
return set(map(dump_to_dynamodb, item))
# may not be a literal instance of list
elif isinstance(item, Iterable):
return list(map(dump_to_dynamodb, item))
# datetime, custom object, None
return item
\Float type which links to this issue. \
+1
Thanks for all above post and code you guys provided and my problem is solved. But I still want to ask why don't we have a setting parameter to disable / enable decimal, use float / int instead?
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
@jamesls Any updates on this issue?
+1
+1
any updates ?
I adapted @flomotlik 's code to support NumberSets (dicts) when reading DynamoDB items :
# Helper class to Decimals in an arbitrary object
def replace_decimals(obj):
if isinstance(obj, list):
for i in range(len(obj)):
obj[i] = replace_decimals(obj[i])
return obj
elif isinstance(obj, dict):
for k, v in obj.items():
obj[k] = replace_decimals(v)
return obj
elif isinstance(obj, set):
return set(replace_decimals(i) for i in obj)
elif isinstance(obj, decimal.Decimal):
if obj % 1 == 0:
return int(obj)
else:
return float(obj)
else:
return obj
+1 on this feature request too
Why is this still open in 2019?
+1
+1
If the main issue you guys are having is JSON serializing the Decimal values, simplejson can handle that. That's what I use in my Lambda functions.
If the main issue you guys are having is JSON serializing the Decimal values, simplejson can handle that. That's what I use in my Lambda functions.
Thanks, nft2! The simplejson package works great.
I use:
import simplejson as json
Instead of:
import json
Hello @jamesls , @garnaat.
It's been 4 years+ since the opening of issue. What is the current take of the boto3 team on this? Are there plans to add support of native python int/float for DynamoDB?
+1 what a pain... sigh
+1 one more here
+1 one more here
Why is this still open in 2019?
2020*
TBH, this Decimal() fiasco has cost me hours (with the net result that I now just use strings for everything and have the app figure it out). I doubt I'll be using dynamodb for any future projects.
Its "type system" is too stupid to live.
If the main issue you guys are having is JSON serializing the Decimal values, simplejson can handle that. That's what I use in my Lambda functions.
Thanks, nft2! The simplejson package works great.
I use:
import simplejson as jsonInstead of:
import json
man ! you saved lot of time. Thanks
2021
+1
Closing in on 5 years since open and the problem is still around :/
Massive +1
This is really a user experience nightmare. Seems like hundreds of users around the web are spending time writing functions (or troubleshooting issues with the json approach, such as NaN handling). Really should be handled inside boto itself.
Here's an updated version of @garnaat's function for Python 3. In my case, I want them as an int, not a float:
def replace_decimals(obj):
"""
Convert all whole number decimals in `obj` to integers
"""
if isinstance(obj, list):
return [replace_decimals(i) for i in obj]
elif isinstance(obj, dict):
return {k: replace_decimals(v) for k, v in obj.items()}
elif isinstance(obj, decimal.Decimal):
return int(obj) if obj % 1 == 0 else obj
return obj
Most helpful comment
FWIW, I use this little function to recurse into Python objects returned by the boto3 DynamoDB resource layer and convert any Decimal values to
intorfloat. It is by no means foolproof and doesn't in any way solve the problem of lack of precision in Python's float type but it solves my problem which is mainly to turn the data into something that can be returned to API Gateway via a Python Lambda function.