Member-only story
Python
Web Apps in Python with Solara — A Streamlit Killer?
The Landscape
Creating web apps entirely in Python is an enticing idea. Setting up a web app requires both frontend and backend skills such as HTML, CSS, JavaScript (and the numerous frameworks) together with Python or some other server-side language on the backend. It’s a lot of work!
If instead everything could be handled entirely in Python, potentially in a single file, the speed of development would increase dramatically. Today, there are several libraries that attempt to deliver this experience. The most popular based on stars on GitHub is Streamlit, with 24.9k stars at the time of writing this.
While Streamlit is incredibly succinct, enabling the creation of web apps in impressively few lines of code, it is not without drawbacks. It has a very limited ability to make customized interfaces, and it has a rather odd mechanism where everything is rerun every time a state is changed. I’ve talked about this in a previous article where I detail when I believe you should and should not use Streamlit:
And look, I don’t dislike Streamlit. I enjoy it when it suits the use case. I’ve written about several apps I’ve built with it:
to name a few. But when you would like to build an app containing complex interfaces and/or nested state, it’s simply not the right tool for the task.
Thus, I’ve been eager to find a new, more customizable framework, that could both bring together the flexibility of frontend frameworks and the succinctness of only using Python in a seamless mix.
Solara
Solara is a recently introduced framework to build web apps in pure Python. In its documentation, it offers at first glance some interesting improvements to Streamlit, such as nested reusable components with their own states that do not re-execute needlessly, and a simple integration with Jupyter Notebooks.
One impressive detail is that the official site of Solara is also built with Solara, which is not true for Streamlit. You can find the official GitHub here and the official site here.
Okay, so in theory it sounds great… but what does the code look like? Is it good in practice? In the end, this is what matters, whether you can efficiently create the apps you want with it.
In the rest of the article, I will test it out by creating something simple, yet complex enough to get a grasp of its ability, a to-do app. I’ve done the same before in Shiny for Python, and now it’s Solara’s turn. Here is an image of the final result:
I will also show a demo at the end of the article where the functionality is displayed. Now, let’s get started.
Coding in Solara
Before beginning, I would also like to add a disclaimer here that, at the time of writing this, the framework is brand new, and I don’t know if things will change as it is developed. Also, I’m in no way an expert in using Solara.
First, I’m defining some global variables:
import solara
text_input = solara.reactive("")
todos = solara.reactive([
{ "text": solara.reactive("Learn Solara"), "done": solara.reactive(False) },
{ "text": solara.reactive("Build a Solara app"), "done": solara.reactive(False) }
])As can be seen, this is performed with solara.reactive(...). For string variables, it is trivial to add a string as the initial value. For the to-dos, on the other hand, I found that adding nested dictionary items, also defined with solara.reactive, seemed to do the trick.
Page()
Thereafter, I’ll define the main component called Page:
@solara.component
def Page():
# adding some css
solara.Style("""
.add-button {
margin-right: 10px;
}
""")
# to center card
with solara.Column(align="center"):
with solara.Card(title="Todo App"):
for todo in todos.value:
Todo(todo)
if len(todos.value) == 0:
solara.Text("No todos yet.")
solara.InputText(label="Add a todo", value=text_input),
solara.Button("Add", on_click=on_add_todo, classes=["primary", "add-button"]),
solara.Button("Remove finished tasks", classes=["secondary"], on_click=clear_finished_todos),
Page()Let’s unwrap it step by step. A component is defined with the decorator @solara.component, in this case, the main component to hold the entire interface. In the component, I’m first calling solara.Style(...) to add some CSS-styles to one of the buttons (see the attribute classes=[..., “add-button”] for that button). If you are not familiar with CSS, don’t worry, this is an uncommon operation, as warned in the documentation:
Note that this is considered an advanced feature, and should be used with caution.
The reason I used it was partially to test the ability of the framework to use commonly used frontend tools, such as CSS.
Thereafter, I’m creating a solara.Column that will center the components inside, and a solara.Card, which will create a styled container.
with solara.Column(align="center"):
with solara.Card(title="Todo App"):
...Next, I loop over the elements in the global todos-state that was defined earlier and pass each state in the list to a separate Todo-component:
for todo in todos.value:
Todo(todo)
if len(todos.value) == 0:
solara.Text("No todos yet.")I will later show the code for the Todo-component.
Finally, there is a text input for inputting new to-dos and two buttons, one to add the to-do and one to remove finished to-dos:
solara.InputText(label="Add a todo", value=text_input),
solara.Button("Add", on_click=on_add_todo, classes=["primary", "add-button"]),
solara.Button("Remove finished tasks", classes=["secondary"], on_click=clear_finished_todos),For the text input, the value is set to one of the global states that will automatically be updated when the element loses focus (or on each keystroke with continuous_update=True).
Both buttons have on_click-callbacks that will handle the logic when they are clicked. Let’s have a look at them:
def on_add_todo():
todos.set(todos.value + [{
"text": solara.Reactive(text_input.value),
"done": solara.Reactive(False)
}])
text_input.set("")
def clear_finished_todos():
todos.set([todo for todo in todos.value if not todo["done"].value])The first method, on_add_todo, will concatenate the old to-do items with the new to-do that takes the value inside the text input, and thereafter clear the text input. Note that the variables todos and text_input are Reactive objects (defined earlier with solara.Reactive(...)) and handle the logic necessary for the states. Thus, to access the actual values they hold at the moment, the .value accessor has to be used.
The second method, clear_finished_todos, will simply iterate through the to-dos and remove those that are “done”. As the dictionary item done was also a Reactive-object, its value is also accessed with .value.
Todo()
The last piece of the puzzle is the Todo-function, a reusable component that each to-do item is rendered with. Here’s the code:
@solara.component
def Todo(todo):
# define a local state, only for this component
editing, set_editing = solara.use_state(False)
# size of 0 will take minimum space
with solara.Columns([1, 0]):
# set background color based on done state
color = "#d6ffd6" if todo["done"].value else "initial"
# some css to make it look nice
with solara.Column(style=f"padding: 1em; width: 400px; background-color: {color};"):
# if editing is true, the item can be edited
if editing:
solara.InputText(label="Edit todo", value=todo["text"])
else:
solara.Checkbox(label=todo["text"].value, value=todo["done"])
# buttons to edit/save and delete
solara.Column(children=[
(
solara.IconButton(icon_name="save", on_click=lambda: set_editing(False))
if editing
else
solara.IconButton(icon_name="edit", on_click=lambda: set_editing(True))
),
solara.IconButton(icon_name="delete", on_click=lambda: todos.set([t for t in todos.value if t != todo]))
])First, solara.use_state(<initial value>) is called to define a local state within the component. If you’ve used functional components in ReactJS, this should be familiar.
Next, two columns are defined:
with solara.Columns([1, 0]):
...One that should stretch (the text) and one that should be of minimum size (the buttons).
The text is shown as follows:
# set background color based on done state
color = "#d6ffd6" if todo["done"].value else "initial"
# some css to make it look nice
with solara.Column(style=f"padding: 1em; width: 400px; background-color: {color};"):
# if editing is true, the item can be edited
if editing:
solara.InputText(label="Edit todo", value=todo["text"])
else:
solara.Checkbox(label=todo["text"].value, value=todo["done"])I add the background-color CSS-style depending on the done-state together with some fixed CSS-styles. The content is thereafter adjusted based on the editing-state to either be a checkbox or a text input. The checkbox, just like the text input, will update the Reactive object in value when clicked.
Finally, we have icon-buttons that are stacked vertically:
# buttons to edit/save and delete
solara.Column(children=[
(
solara.IconButton(icon_name="save", on_click=lambda: set_editing(False))
if editing
else
solara.IconButton(icon_name="edit", on_click=lambda: set_editing(True))
),
solara.IconButton(icon_name="delete", on_click=lambda: todos.set([t for t in todos.value if t != todo]))
])When editing a save-button is shown, otherwise an edit button. Additionally, there is a delete-button that will remove the current to-do.
That’s it! Here’s a demo of the final result:
Note that if a checkbox is clicked within the Todo-component, only the Todo-component is updated, not the Page-component. This enables more efficient programs.
Here’s all the code:
import solara
text_input = solara.reactive("")
todos = solara.reactive([
{ "text": solara.reactive("Learn Solara"), "done": solara.reactive(False) },
{ "text": solara.reactive("Build a Solara app"), "done": solara.reactive(False) }
])
def on_add_todo():
todos.set(todos.value + [{
"text": solara.Reactive(text_input.value),
"done": solara.Reactive(False)
}])
text_input.set("")
def clear_finished_todos():
todos.set([todo for todo in todos.value if not todo["done"].value])
@solara.component
def Todo(todo):
# define a local state, only for this component
editing, set_editing = solara.use_state(False)
# size of 0 will take minimum space
with solara.Columns([1, 0]):
# set background color based on done state
color = "#d6ffd6" if todo["done"].value else "initial"
# some css to make it look nice
with solara.Column(style=f"padding: 1em; width: 400px; background-color: {color};"):
# if editing is true, the item can be edited
if editing:
solara.InputText(label="Edit todo", value=todo["text"])
else:
solara.Checkbox(label=todo["text"].value, value=todo["done"])
# buttons to edit/save and delete
solara.Column(children=[
(
solara.IconButton(icon_name="save", on_click=lambda: set_editing(False))
if editing
else
solara.IconButton(icon_name="edit", on_click=lambda: set_editing(True))
),
solara.IconButton(icon_name="delete", on_click=lambda: todos.set([t for t in todos.value if t != todo]))
])
@solara.component
def Page():
solara.Style("""
.add-button {
margin-right: 10px;
}
""")
# to center card
with solara.Column(align="center"):
with solara.Card(title="Todo App"):
for todo in todos.value:
Todo(todo)
if len(todos.value) == 0:
solara.Text("No todos yet.")
solara.InputText(label="Add a todo", value=text_input),
solara.Button("Add", on_click=on_add_todo, classes=["primary", "add-button"]),
solara.Button("Remove finished tasks", classes=["secondary"], on_click=clear_finished_todos),
Page()Running a Solara App in Jupyter Notebook
The code can also easily be run within Jupyter Notebooks by simply pasting the code in a cell and running it:
Conclusion
From testing out Solara, I’ve found that it includes some nice benefits that are not available in Streamlit, such as reusable components, more complex functionality with local and global states and more customizability, with CSS for instance.
Still, there are a few things that I feel are missing. For one, I would like to be able to use basic HTML components. Currently, you can call solara.HTML(...) but it is only for read-only rendering of HTML in isolation, you cannot wrap other components within them. Additionally, some other basic components are currently missing, like multiline text inputs, but these will likely be added sooner or later.
Overall, it has great potential, and will likely replace my use of Streamlit. But at the moment it will not replace my use of a ReactJS clientside and a Python serverside in situations where I would like to have complete flexibility.
If you enjoyed this article:
- 👏 Clap, this will help me understand what my readers like and want more of.
- 🙏 Follow or subscribe, if you would like to read my upcoming articles, new ones every week!
- 📚 If you are looking for more content, check out my reading lists in AI, Python or Data Science.
Thanks for reading and have a great day.






