Getting values with the right type in Redis

Question:

I’m using redis in my python application to store simple values like counters and time stamp lists, but trying to get a counter and comparing it with a number I came across a problem.

If I do:

import redis
...
myserver = redis.Redis("localhost")
myserver.set('counter', 5)

and then try to get that value like this:

if myserver.get('counter') < 10:
     myserver.incr('counter')

then I get a type error in the if statement because I’m comparing ‘5’ < 10, which means I’m storing an integer value and getting a string one (which can be considered as a different value).

My question is: is this supposed to work like that? I mean its a very basic type, I understand if I have to parse objects but an int? Seems that I’m doing something wrong.

Is there any configuration I’m missing?

Is there any way to make redis return the right type and not always a string?
I say this because its the same for lists and datetimes or even floating point values.

Could this be a problem with the redis-py client I’m using and not redis itself?

Asked By: jeruki

||

Answers:

Technically speaking you need to take care of that on your own.

However, have a look at this link, especially at the part of their README that refers to parsers and response callbacks, maybe that’s something you can use. Question would be whether this is an overkill for you or not.

Answered By: favoretti

Looks like that is how redis stores its data:

redis 127.0.0.1:6379> set counter 5
OK
redis 127.0.0.1:6379> type counter
string
redis 127.0.0.1:6379> incr counter
(integer) 6
redis 127.0.0.1:6379> type counter
string

If you really want to, you could probably monkeypatch the redis-py client to infer data types.

Answered By: Joel Cornett

As @favoretti said, response callbacks will do the trick. It’s not complicate at all, just one line and all will be taken care of.

In [2]: import redis
In [3]: r = redis.Redis()
In [10]: r.set_response_callback('HGET', float)
In [11]: r.hget('myhash', 'field0')
Out[11]: 4.6

for hmget, it returns a list of strings, not one single string, so you need to construct a little more comprehensive callback function:

In [12]: r.set_response_callback('HMGET', lambda l: [float(i) for i in l])

In [13]: r.hmget('myhash', 'field0')
Out[13]: [4.6]

same for hgetall.

Answered By: pingz

Here is my test. Two redis connections: one returns int type, the other float

import redis

age_field = redis.Redis()
age_field.set_response_callback('HGET', int)
age_field.hget('foo', 'age')
# OUT: 40
a =age_field.hget('foo', 'age')
type(a)
# OUT: <type 'int'>

gpa_field = redis.Redis()
gpa_field.set_response_callback('HGET', float)
gpa_field.hget('foo', 'gpa')
# OUT: 2.5
b = gpa_field.hget('foo', 'gpa')
type(b)
# OUT: <type 'float'>
Answered By: Jie Bao

You can set decode_respone like True

redis.StrictRedis(host="localhost", port=6379, db=0, decode_responses=True)
Answered By: Lucas Picoloto

Though leveraging set_response_callback is fine for simple data types, if you’re wondering about the fastest-and-easiest route to storing things like dictionaries, list, tuples – and preserving the python-native datatypes they may-or-may-not contain – I recommend using python’s built-in pickle library:

# Imports and simplified client setup
>>> import pickle
>>> import redis
>>> client = redis.Redis()
# Store a dictionary
>>> to_store = {'a': 1, 'b': 'A string!', 'c': [1, True, False, 14.4]}
>>> client.set('TestKey', pickle.dumps(to_store))
True
# Retrieve the dictionary you just stored.
>>> retrieved = pickle.loads(client.get('TestKey'))
{'a': 1, 'b': 'A string!', 'c': [1, True, False, 14.4]}

Here’s a simple client that will reduce the pickle boilerplate in the above example and provide you with a clean interface for storing and retrieving native python datatypes to and from Redis:

"""Redis cache."""
import pickle
import redis

redis_host = redis.Redis()


class PythonNativeRedisClient(object):
    """A simple redis client for storing and retrieving native python datatypes."""

    def __init__(self, redis_host=redis_host):
        """Initialize client."""
        self.client = redis_host

    def set(self, key, value, **kwargs):
        """Store a value in Redis."""
        return self.client.set(key, pickle.dumps(value), **kwargs)

    def get(self, key):
        """Retrieve a value from Redis."""
        val = self.client.get(key)
        if val:
            return pickle.loads(val)
        return None

redis_client = PythonNativeRedisClient()

Usage:

>>> from some_module import redis_client
>>> to_store = {'a': 1, 'b': 'A string!', 'c': [1, True, False, 14.4]}
>>> redis_client.set('TestKey', to_store)
True
>>> retrieve = redis_client.get('TestKey')
{'a': 1, 'b': 'A string!', 'c': [1, True, False, 14.4]}
Answered By: respondcreate

Depending on needs, we have 3 ways to store data into Redis

String

  • All data are stored and read back as bytes.
  • This is the native way method that used by Redis.
  • We could use redis-cli to inspect and manipulate the data
  • However, data may not be stored efficiently. For example 12345 is stored as string using at least 5 bytes even it could be represented by a 2-byte unsigned short.
  • You could limited to use various data structures offered by Redis.
  • The data stored within Redis is portable across different versions of Python and programming languages.

Pickle

  • All Python data are converted into binary data using pickle library before stored into Redis.
  • Data must be converted back to Python data types using pickle library before could be used.
  • It could model complex and dynamic data structures not offered by Redis.
  • Once the data is converted back to Python data types, We could use powerful Python to manipulate the data.
  • It uses more memory within Redis as pickle includes meta data, references in its output.
  • It is not possible to use redis-cli to manipulate the data, such as INCR.
  • The data stored within Redis is not portable across different versions of Python and programming languages.

Struct

  • All Python data are converted into binary data using struct library before stored into Redis.
  • Data must be converted back to Python data types using struct library before could be used.
  • It could model complex and static data structures not offered by Redis.
  • Once the data is converted back to Python data types, We could use powerful Python to manipulate the data.
  • It uses the least memory out of these 3 methods
  • It is not possible to use redis-cli to manipulate the data, such as INCR.
  • The data stored within Redis is portable across different versions of Python and programming languages.
from redis import Redis
import sys
import pickle
import struct

redis = Redis()

# Data
d = [1234, 3.1415, b'Good', True]
k = ['int', 'float', 'byte', 'bool']

## Using String
print(f'Stringn{"_"*50}')
# Need to convert True to string or int or float
d2 = [1234, 3.1415, b'Good', int(True)]
redis.hset('k0', mapping=dict(zip(k,d2)))
print(redis.hmget('k0', *k))
print()


## Using Pickle
print(f'Picklen{"_"*50}')
packed = pickle.dumps(d)
print(f'Pickled data occupies {sys.getsizeof(packed)} bytes')
print(packed)
redis.set('k1', packed)
print(f'{pickle.loads(redis.get("k1"))}')
print()


## Using Struct
print(f'Structn{"_"*50}')
s = struct.Struct('H f 10s ?')
print(f'The structure occupies {s.size} bytes.')
packed = s.pack(*d)
print(f'{packed}')
redis.set('k2', s.pack(*d))
print(f'{s.unpack(redis.get("k2"))}')

Expected output:

String
__________________________________________________
[b'1234', b'3.1415', b'Good', b'1']

Pickle
__________________________________________________
Pickled data occupies 69 bytes
b'x80x04x95x19x00x00x00x00x00x00x00]x94(Mxd2x04G@t!xcaxc0x83x12oCx04Goodx94x88e.'
[1234, 3.1415, b'Good', True]

Struct
__________________________________________________
The structure occupies 19 bytes.
b'xd2x04x00x00Vx0eI@Goodx00x00x00x00x00x00x01'
(1234, 3.1414999961853027, b'Goodx00x00x00x00x00x00', True)
Answered By: yoonghm

one solution could be to use a json.loads() to return int or float as a correct type, instead of str.

import json
import redis

redis = redis.from_url("redis://************")

def get(key, decode=True):
    """set decode to False when value stored as a string"""
    value = redis.get(key)
    if not decode:
        return value
    if value is not None:
        try:
            return json.loads(value)
        except json.decoder.JSONDecodeError:
            # not containing a JSON document
            return value

redis.set("foo", 2)
redis.set("bar", 2.75)

get("foo")
# 2
get("bar")
# 2.75
Answered By: Mojtaba Arvin