Stores

An interactive application needs some way of creating variables that capture the state of the application. This is important to

  • keep track of application state that is going to change over time

  • keep this state in sync between the frontend and Python code

  • provide explicit ways to manipulate this state

  • implement and debug the application in terms of this state

In Meerkat, wrapping a Python object in a Store object provides a way to do this. These pages will explain:

  • what a Store is

  • how to work with Store objects

  • how Store objects are used in interactive applications

  • common pitfalls and how to avoid them

What is a Store?

Important

A Store behaves like the object it wraps, with extra functionality for reactivity.

A Store is a special object provided by Meerkat that can be used to wrap arbitrary Python objects, such primitive types (e.g. int, str, list, dict), third-party objects (e.g. pandas.DataFrame, pathlib.Path), and even your custom objects.

For example, a Store can be used to wrap a string:

x = mk.Store("Hello World!")

How do we use a Store?

Important

Store objects are designed to be as transparent as possible, so that they behave like the object they wrap.

All attributes and methods of the wrapped object are exposed through the store.

For example, we can call .lower() on a Store wrapping a str.

x = mk.Store('HELLO WORLD')
y = x.lower() # 'hello world'
type(y) # str

Store objects also behave exactly like the object they wrap when you access their attributes i.e. you use .attribute syntax. This is quite useful when wrapping complex objects, like pandas.DataFrame.

import pandas as pd
x = mk.Store(pd.DataFrame({'a': [1, 2, 3]}))
y = x.columns
type(y) # <class 'pandas.core.indexes.base.Index'>

Using functions with Stores

Store objects behave (almost) exactly like the object they wrap when you use them as inputs to functions. For instance, the following code works as expected.

x = mk.Store('HELLO WORLD')
y = len(x) # 11
type(y) # int

However, sometimes Store objects may behave unexpectedly when used with arbitrary functions. This mostly happens when functions are not duck-typed, but instead have behavior that expects specific types. It can also happen when a function is actually implemented in C.

Here’s a simple example that fails from Python’s os module.

x = mk.Store('./relative/path/to/file')
y = os.path.abspath(x)
# TypeError: expected str, bytes or os.PathLike object, not Store

The workaround is simple. If you encounter an error of this kind, use the .value attribute to get the underlying object and pass that in to the function instead.

x = mk.Store('./relative/path/to/file')
y = os.path.abspath(x.value)
type(y) # str

Why do we need Stores?

As we saw earlier, stores are essential for building interactive interfaces - i.e. reactivity and endpoints.

Important

Stores are what make it possible to build reactive functions with arbitrary Python objects.

Why do we need Stores?

The defacto way to mark an object is to wrap it in a Store. In this section, we will briefly cover the importance of Stores. You can read the full guide on Stores at [XXXXXXXXXXXX].

A Store is a special object provided by Meerkat that can be used to wrap arbitrary Python objects, such primitive types (e.g. int, str, list, dict), third-party objects (e.g. pandas.DataFrame, pathlib.Path), and even your custom objects. A major reason to use Store objects is that they make it possible for Meerkat to track changes to Python objects.

Let’s look at an example to understand why they are so important to reactive functions.

@mk.reactive() def add(a, b): return a + b Now, let’s try something that might feel natural. Create two int objects, call add with them, and then change one of them.

x = 1 y = 2 z = add(x, y) # z is 3

x = 4 # z is still 3 You might think for a second that z should be updated to 6 because x changed and add is a reactive function. This is not the case.

This is because x is just an int. By changing x, we aren’t changing the object that x points to (i.e. the int 1). Instead, we are just changing the variable x to point to a different object.

What we need here is to update the object that x points to. It’s impossible to do this with a regular int. We can do this with a Store instead.

x = mk.Store(1) y = mk.Store(2) z = add(x, y) # z is Store(3), type(z) is Store

x.set(4, triggers=True) print(z)

z is now Store(6), type(z) is Store

By calling set() on x, we are changing the object that x points to. This is what allows z to be updated. Ignore the triggers=True argument for now, we discuss it in more detail in the section below.)

Store is a transparent wrapper around the object it wraps, so you can use that object as if the Store wasn’t there.

x = mk.Store(1)
y = mk.Store(2)
z = x + y # z is Store(3), type(z) is Store

message = mk.Store("hello")
message = message + " world"

# message is Store("hello world"), type(message) is Store

The takeaways are:

  • A Store can wrap arbitrary Python objects.

  • A Store will behave like the object it wraps.

  • A Store is necessary to track changes when passing an object to a reactive function.