Exploring Solara: A New Era for Python Web Applications
Written on
Chapter 1: Introduction to Python Web Apps
The concept of developing web applications solely using Python is highly appealing. Creating a web app typically involves mastering both frontend and backend technologies like HTML, CSS, JavaScript, and a server-side language like Python. This can be quite cumbersome!
If we could manage everything using just Python, potentially in a single script, the development process could be significantly expedited. Currently, several libraries aim to provide this streamlined experience. Among them, Streamlit stands out, boasting 24.9k stars on GitHub at the time of writing.
Despite Streamlit's remarkable brevity, which allows for the creation of web apps with minimal code, it comes with limitations. For instance, it offers restricted capabilities for customizing interfaces and employs a peculiar method where the entire app is rerun whenever a state changes. I've previously discussed this in another article, detailing when Streamlit is a suitable choice and when it isn't.
While I appreciate Streamlit for specific projects—having built several applications like an AI Website Generator and a Dividend Investing Dashboard—it's not the ideal choice for apps that require intricate interfaces or complex state management.
Thus, I have been on the lookout for a more adaptable framework that combines the flexibility of frontend technologies with the simplicity of using only Python. Enter Solara.
Chapter 2: Unveiling Solara
Solara is a newly launched framework designed for building web applications entirely in Python. Its documentation presents several appealing enhancements over Streamlit, including nested reusable components with independent states that avoid unnecessary re-execution, and seamless integration with Jupyter Notebooks.
But how does Solara perform in practice? Is it effective for building the applications you envision? To answer these questions, I will create a straightforward yet sufficiently intricate to-do application, a task I've tackled previously with Shiny for Python.
Video Description: Watch how to quickly build a Python web app in just 3 minutes with Solara, a promising alternative to Streamlit.
Creating a Simple To-Do App
Before diving into the coding aspect, I want to clarify that, as of this writing, Solara is still in its early stages, and I am not claiming to be an expert in it.
First, I will establish 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) }
])
Here, we define text_input and todos using solara.reactive(...). For string variables, initializing a value is straightforward, but for nested dictionary items like todos, we can also use solara.reactive.
Next, I will define the primary component named Page:
@solara.component
def Page():
solara.Style("""
.add-button {
margin-right: 10px;}
""")
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()
In this code, we first incorporate some CSS for styling the buttons. The Page component centers the entire interface within a card and loops through each to-do item.
Let's explore the on_add_todo and clear_finished_todos functions:
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 on_add_todo function appends a new to-do item based on the input value and resets the input field. The clear_finished_todos function filters out completed tasks.
Now, let's look at the Todo component, which renders each to-do item:
@solara.component
def Todo(todo):
editing, set_editing = solara.use_state(False)
with solara.Columns([1, 0]):
color = "#d6ffd6" if todo["done"].value else "initial"
with solara.Column(style=f"padding: 1em; width: 400px; background-color: {color};"):
if editing:
solara.InputText(label="Edit todo", value=todo["text"])else:
solara.Checkbox(label=todo["text"].value, value=todo["done"])
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]))
])
This component manages local state for editing each to-do and provides options to either save or delete tasks.
The final code for the entire application looks like this:
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):
editing, set_editing = solara.use_state(False)
with solara.Columns([1, 0]):
color = "#d6ffd6" if todo["done"].value else "initial"
with solara.Column(style=f"padding: 1em; width: 400px; background-color: {color};"):
if editing:
solara.InputText(label="Edit todo", value=todo["text"])else:
solara.Checkbox(label=todo["text"].value, value=todo["done"])
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;}
""")
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 Application in Jupyter Notebook
The code can also be easily executed within a Jupyter Notebook by simply pasting it into a cell and executing it.
Chapter 3: Conclusion
Through my exploration of Solara, I've identified several advantages over Streamlit, such as reusable components, enhanced functionality with local and global states, and greater customization options, including CSS.
However, I believe there are some areas for improvement. For instance, the ability to use basic HTML components would be beneficial. Currently, you can utilize solara.HTML(...), but it's limited to read-only HTML rendering, and you can't encapsulate other components within it. Additionally, some fundamental components, like multiline text inputs, are still absent, though they may be added in the future.
In summary, Solara exhibits significant potential and may eventually replace my use of Streamlit. However, I will continue to rely on a ReactJS frontend paired with a Python backend in scenarios where I require complete flexibility.
If you found this article valuable, please consider giving it a clap to show your support! You can also follow me for more articles published weekly. For additional reading material, check out my curated lists in AI, Python, or Data Science.
Thank you for reading, and have a wonderful day!
Video Description: Discover how to create a robust and scalable Jupyter/Web app using Solara, a pure Python, React-style web framework.