Tracking breastfeeding data using Python x NiceGUI
Lessons learned about using NiceGUI as a Python-based UI Framework to develop a breastfeeding data tracking web app.
Introduction
Through a project at work involving python code, I had discovered that streamlit was an amazing tool to quickly create web apps to visualize datasets and test out their transformation by python scripts.
With streamlit, you can with a very few lines, present data from a csv file with UI elements that are simple to use and nice-looking by default:
import streamlit as st
import pandas as pd
st.write("""
# My first app
Hello *world!*
""")
df = pd.read_csv("my_data.csv")
st.line_chart(df)
However, I also noticed that Streamlit had limitations, the most important one being that the scripts are rerun each time the state is changed, i.e. when the user changes an input parameter, or clicks on a button to trigger an action. That works for simple cases, but not for more complex apps where you need to:
- deal with randomness
- handle user interaction without re-running everything (despite Streamlit's caching system)
- manage multiple event loops
- or manage the state in a more advanced way, for instance to take into account multiple users or sessions.
In search of an alternative, I found NiceGUI, a Python UI framework that precisely tries to address these shortcomings of Streamlit.
In a coding style that it rather similar to its predecessor, NiceGUI introduces:
- callbacks to handle user interactions and preserve state,
- props to customize the Quasar-based ui elements,
- and tailwind classes to style them.
Here is an example:
from nicegui import ui
from nicegui.events import ValueChangeEventArguments
def show(event: ValueChangeEventArguments):
name = type(event.sender).__name__
ui.notify(f'{name}: {event.value}')
ui.button('Button', on_click=lambda: ui.notify('Click'))
with ui.row():
ui.checkbox('Checkbox', on_change=show)
ui.switch('Switch', on_change=show)
ui.radio(['A', 'B', 'C'], value='A', on_change=show).props('inline')
with ui.row():
ui.input('Text input', on_change=show)
ui.select(['One', 'Two'], value='One', on_change=show)
ui.link('And many more...', '/documentation').classes('mt-8')
ui.run()
It sounded promising, and I just needed a project to try it out π
The project
The idea came when I thought it'd be nice to track and visualize breastfeeding meal statistics. I had started in the most straight-forward way, by storing data in an excel spreadsheet. But manually updating the sheet and plots as days went by became quickly cumbersome: it was the perfect opportunity (and the perfect excuse) to better use my sleepless nights, while playing around with this framework.
Although the immediate thought was that I could test how enjoyable it would be to create and deploy a web app based on python-based NiceGUI, the project quickly turned into an opportunity to progress my python development skills and explore some best practices for software development, such as:
- managing a python environment using micromamba
- formatting and linting with ruff
- typing python code and checking it with mypy
- split code using a model-view-controller design pattern
- validate data using pydantic models
- testing the code prior to releasing using pytest
- calculating the coverage of tests using coverage
- deploying with GitHub Actions
- keep track of changes in a visually appealing changelog using git-cliff
It was the perfect opportunity (and the perfect excuse) to better use my sleepless nights, while playing around with the NiceGUI framework.
The result: milk-tracker
With 500 lines of code, I obtained a Minimum Viable Product. A few features have been added since, and the code is now made of about 1800 lines and 550 testing lines covering 27% of the code, and I feel like I managed to create a decent app to track meals and log memories. Among the features:
- Access via my tmlmt.com domain, encrypted in https and protected by password
- Recording of new meals, locking start time, counting rounds
- Keeping track of daily Vitamin intake for the baby and the mother
- Graphs to visualize all meals from the last 3 days
- Data table to see the details of the last 10 meals
- Daily statistics of meal count, duration and intervals
- Recording of daily "memories"
The code is open sourced on Github with an MIT license: feel free to consult the repository to check the details. The readme file contains instructions on how to host and run it on your own server π
Lessons learned
NiceGUI: strengths and limitations
Overall, I had a good time using the framework and am happy I used it for this project. The maintainers are friendly, and actively developing it while supporting the community.
Deep-diving into key strengths and limitations, I would mention the following:
- NiceGUI is based on FastAPI / Starlette + Uvicorn for the backend, and Vue (props, etc.) + Quasar (components) + Tailwind (styling) for the frontend. Which in itself has its pros and cons. One can leverage the power and customization potential of those great packages, but despite a rather good NiceGUI documentation, it does require some time to get acquainted with them.
- The callback system (
onclick
,onchange
etc.) and built-in storage functionalities (with different scopes: tab / browser / server) really enable to take control of the app. But if you want to keep the readability of your code at an acceptable level as the app grows, you need to structure your code by yourself.
- There is a rich and well-thought library of UI elements, but some things could sometimes be better thought through end-to-end, like displaying and updating a DataFrame in a table (see the Pull Request I submitted).
- Binding of certain properties can be done to scalars or shallow dictionaries, but you canβt easily use Vueβs reactivity patterns (reactive variables, computed refs, etc) without using a third-party extension. That makes the handling of changes in certain input parameters quite verbose.
- The framework enables to quickly create apps to work with data or embedded systems (which is one of the main activities of the maintainers behind NiceGUI), but it is not a full fledge solution to create complex apps for the public: things like SEO, Security, etc. is much better handled by a plugin/module-based web app framework such as Nuxt (see next section)
Python vs Javascript/Typescript
I thought my need was simple enough to be covered by NiceGUI while keeping the code elegant. And it was indeed very fast to reach a minimum viable product (i.e displaying a list of meals in a table and a few graphs); that is actually the best thing about it. But it turned out that either my overall need was complex after all (I wanted multiple specific functionalities), or that NiceGUI was not necessarily the best fit for the level of development experience I was looking for.
To build full fledge web applications, I find that Typescript-based frameworks, such as Nuxt β that I used to create the Bodega Map (separate article to come later) β offer a much better developer experience despite the little extra effort and start-up cost in styling the UI to a satisfactory level:
- For typing code, Python + mypy is nowhere close to Typescript which is faster (check at every keystroke vs at file save with mypy) and richer.
- For testing, I find vitest much powerful than pytest. An example being the management of snapshots and fixtures.
- Nuxt and its folder structure, auto-imports, rich ecosystem of modules (as well as the already rich javascript packages ecosystem) and basis on Vue (reactivity/binding) constitute all in all a much toolset and offers a better development experience.
Micromamba is much better than conda
A nice extra in all of this was the discovery of micromamba as python environment manager. Although it still lacks a few desirable functionalities (exporting pip packages, ) and a wider adoption by Python package developers (some donβt publish to conda-forge), it is lightweight and infinitely faster than conda. The speed is also perceptible when using it as environment manager for my Github Deploy Action.
Conclusion
All in all, it was a great learning experience to create my breastfeeding tracker based on NiceGUI. While being aware of the limitations Iβve been facing with it, I am also grateful of its strengths, that I could count on to actually obtain a β¦ nice GUI for the need I've had.
It is really incredible to see the diversity of languages (Python, Typescript, Go, Rust, C++, etc.) and frameworks that exist out there and I hope to be able to continue playing around with them as they develop, and keep that secret coding skill of mine.