Streamlit — skrypty Pythona jako interaktywne aplikacje webowe
Streamlit wykonuje cały twój skrypt od nowa przy każdym kliknięciu. To brzmi jak wada, a jest źródłem jego prostoty — i całej listy pułapek. Najnowsze wydania dodały trzy mechanizmy, które te pułapki wprost adresują.
Streamlit wygrał w swojej kategorii, bo odpowiedział na pytanie, którego analitycy danych nie chcieli zadawać: czy naprawdę muszę uczyć się frontendu, żeby ktoś inny mógł kliknąć w mój wykres?
Odpowiedź brzmi „nie", a cena tej odpowiedzi jest jedną z najodważniejszych decyzji projektowych, jakie widziałem. Streamlit nie ma modelu komponentów, nie ma drzewa stanu, nie ma cyklu życia. Ma coś zupełnie innego: przy każdej interakcji użytkownika wykonuje twój skrypt od pierwszej linii do ostatniej. Od nowa. Za każdym razem.
Cały ten wpis jest o konsekwencjach tego jednego zdania — bo kto je zrozumie, przestanie walczyć z tym narzędziem. A ostatnie wydania dodały trzy mechanizmy, które te konsekwencje wprost łagodzą, i o nich mówi się zdecydowanie za mało.
Stan projektu
streamlit/streamlit ma 45 731 gwiazdek i 4375 forków, licencję Apache 2.0 i historię od sierpnia 2019. Wersja 1.63.0 wyszła 1 września 2026, a wcześniejsze — 1.62, 1.61.1, 1.61, 1.60 — mniej więcej co dwa tygodnie. Rytm jest miarowy i utrzymywany od lat, bez przestojów.
Wymagany Python to 3.10 lub nowszy. Gałąź domyślna nazywa się develop, nie main — drobiazg, ale wystarczy, by zepsuć skrypt automatyzacji, który zakłada inaczej.
Projekt należy do Snowflake i nie jest to informacja z dziennikarskiego przecieku — widać ją w historii commitów, gdzie znaczna część autorów ma nazwy z firmowym przedrostkiem. Dla oceny trwałości jest to raczej dobra wiadomość: za rozwojem stoi opłacany zespół, a nie wolontariat. Warto natomiast pamiętać, że kierunek rozwoju wyznacza firma mająca własny produkt chmurowy, a Apache 2.0 zabezpiecza kod już wydany, nie przyszłe decyzje.
Uczciwie: 1162 otwarte zgłoszenia. Przy tej skali i wieku to normalne, ale nie licz na szybką reakcję na własne.
Dlaczego przebieg od nowa to dobry pomysł
Model „wykonaj wszystko jeszcze raz" wygląda na marnotrawstwo, dopóki nie zobaczy się, co eliminuje. Nie ma synchronizacji stanu między widokiem a danymi, bo nie ma trwałego widoku. Nie ma pytania, co przerysować, bo przerysowuje się wszystko. Nie ma programowania zdarzeniowego, bo nie ma zdarzeń — jest tylko skrypt czytany z góry na dół.
Dzięki temu kod aplikacji Streamlit czyta się jak notatnik: wczytaj dane, policz, narysuj. Suwak nie jest obiektem z nasłuchem, tylko funkcją zwracającą liczbę. To dlatego osoba, która nigdy nie widziała Reacta, robi w tym działający panel w godzinę.
Rachunek za to wystawia się w trzech miejscach: kosztowne obliczenia powtarzają się bez potrzeby, zmienne lokalne nie przeżywają kliknięcia, a każda interakcja odświeża całą stronę. Na każde z tych trzech Streamlit ma odpowiedź.
Trzy dźwignie
Pamięć podręczna dzieli się na dwie, i to rozróżnienie jest ważniejsze, niż wygląda. st.cache_data obsługuje dane — wyniki obliczeń i zapytań, przekazywane jako kopie, więc modyfikacja zwróconego obiektu nie psuje wpisu w pamięci. st.cache_resource obsługuje zasoby, których kopiować nie wolno i nie da się: połączenie z bazą, wczytany model, klienta usługi zewnętrznej. Ten drugi zwraca ten sam obiekt wszystkim sesjom, co jest dokładnie tym, czego się chce przy połączeniu z bazą — i dokładnie tym, czego nie chce się przy danych użytkownika. Pomylenie ich to najczęstszy poważny błąd w tym narzędziu. W wydaniu 1.63 doszło zresztą konfigurowalne odświeżanie pamięci w tle.
Stan sesji — st.session_state — to słownik przeżywający kolejne przebiegi. Wszystko, co ma pamiętać wartość między kliknięciami, musi tam trafić, bo zmienna lokalna zniknie.
Fragmenty są najciekawsze. Dekorator @st.fragment sprawia, że interakcja z widżetem wewnątrz funkcji przerysowuje tylko tę funkcję, a nie całą aplikację. Dwa parametry warto znać:
run_everyodświeża fragment cyklicznie, nawet gdy nikt nic nie klika — przyjmuje sekundy, obiekt różnicy czasu albo napis w rodzaju"1h23s". To najprostsza droga do panelu odświeżającego się samodzielnie.parallel=Truezleca fragment puli wątków, więc przy pełnym przebiegu może wykonywać się równolegle z resztą skryptu. Do niezależnych, wolnych operacji jest to realne przyspieszenie.
Przy zrównolegleniu jest jednak zastrzeżenie, które trzeba znać z góry: podczas przebiegu równoległego część poleceń jest zablokowana, bo nie są bezpieczne w wątkach — okna dialogowe, przełączanie stron i zapis do kontenerów utworzonych poza fragmentem. W przebiegach sekwencyjnych, czyli po interakcji z widżetem, działają normalnie. Dokumentacja mówi też wprost, dlaczego tak: przebiegi ograniczone do jednego fragmentu pozostają sekwencyjne, żeby aktualizacje stanu były przewidywalne.
Nowość, która zmienia odruchy: „nie przeliczaj"
A teraz rzecz, która w moich oczach jest najważniejszą zmianą w tym narzędziu od czasu fragmentów, a przechodzi bez rozgłosu.
Widżety przyjmują parametr on_change, w którym można podać wywołanie zwrotne. W nowych wydaniach doszła wartość on_change="ignore" — najpierw dla suwaka, w 1.63 także dla pola tekstowego. Znaczy ona: nie przeliczaj aplikacji, gdy użytkownik zmieni tę wartość. Widżet aktualizuje się w przeglądarce, a Python zobaczy nową wartość przy następnym przebiegu wywołanym czymkolwiek innym.
To rozwiązuje najbardziej irytujące zachowanie Streamlita: suwak, który przy każdym drgnięciu myszy uruchamia trzydziestosekundowe zapytanie. Dotąd ratunkiem był formularz z przyciskiem zatwierdzenia; teraz można zostawić widżety swobodne, a przeliczenie wywołać jednym przyciskiem.
Jest jednak haczyk, który dokumentacja podaje uczciwie i który trzeba wpisać do projektu: zignorowane zmiany są trzymane w przeglądarce i przepadają, jeśli użytkownik odświeży stronę przed tym kolejnym przebiegiem. Chyba że skorzystasz z mechanizmu opisanego niżej.
Adresy URL, które da się wysłać koledze
Drugi nowy parametr — bind="query-params" — wiąże wartość widżetu z parametrem w adresie strony. Zmiana widżetu aktualizuje adres, a adres potrafi ustawić widżet przy wejściu. Nazwą parametru staje się klucz widżetu, więc key jest wymagany.
Znaczenie praktyczne jest większe niż wygoda. Panel, którego stan siedzi w adresie, można wysłać komuś w takiej postaci, w jakiej się go widzi — z ustawionymi filtrami, zakresem dat i wybranym oddziałem. Kto pracował z panelami rozsyłanymi w firmie, wie, że to różnica między „zobacz sam" a „popatrz na to".
Zasady, które warto znać, bo są nieoczywiste:
- Gdy wartość równa się domyślnej, parametr znika z adresu — dla czystości linku.
- Powiązanego parametru nie da się zmienić ani usunąć przez
st.query_params; jedyną drogą programową jest stan sesji. - Nieprawidłowe wartości są ignorowane i usuwane z adresu — czyli podrzucony w linku śmieć nie wywali aplikacji.
- Suwaki zakresowe używają powtórzonego parametru, na przykład
?price=10&price=90. - W połączeniu z
on_change="ignore"adres aktualizuje się natychmiast, choć Python dowie się o zmianie później. To właśnie zamyka lukę z gubieniem wartości przy odświeżeniu.
Wartości, które nie giną przy zmianie strony
Trzecia nowość, persist_state, dotyczy problemu, na który natknie się każdy budujący aplikację wielostronicową. Domyślnie wartość widżetu przepada, gdy widżet przestaje być rysowany — bo schowałeś go warunkowo albo użytkownik przeszedł na inną stronę.
Parametr przyjmuje dwie wartości. "page" zachowuje wartość, dopóki użytkownik zostaje na stronie, gdzie widżet jest zdefiniowany — czyli ratuje przypadek warunkowego ukrycia, ale nie przetrwa przejścia i nie wróci po powrocie. "session" zachowuje ją na całą sesję, także przy przechodzeniu między stronami, więc wraca po powrocie. Wymaga ustawionego klucza. Jeżeli włączone jest też wiązanie z adresem, ono ma pierwszeństwo — wartość siedzi wtedy w URL-u i przeżywa niezależnie od zasięgu.
Te trzy parametry razem — ignorowanie zmian, wiązanie z adresem i trwałość stanu — czytam jako świadomą odpowiedź projektu na skargi zbierane latami. Warto sprawdzić, czy własne obejścia, które sobie po drodze napisałeś, nie są już zbędne.
Uwierzytelnianie, czyli koniec „nikt nie zna tego adresu"
Osobny temat, który ma dla polskiej firmy większe znaczenie niż wszystko powyżej. Panele Streamlita długo nie miały żadnego mechanizmu logowania, więc wewnętrzne narzędzia z danymi klientów chroniło się „tym, że nikt nie zna adresu". To nie jest zabezpieczenie, a przy danych osobowych bywa problemem także formalnym.
Dziś w interfejsie są st.login(), st.logout() i st.user, oparte o OpenID Connect. Konkrety, które sprawdziłem w źródłach:
- Tylko OIDC, nie ogólny OAuth 2.0. Google, Microsoft, Okta i inni dostawcy tożsamości — tak; dowolny dostawca OAuth-a — nie.
- Wymaga biblioteki
Authlibw wersji 1.3.2 lub nowszej, instalowanej jako dodatek:pip install streamlit[auth]. - Konfiguracja idzie w sekcji
[auth]pliku sekretów: adres powrotu z ścieżkąoauth2callback, mocny losowy sekret ciasteczka, identyfikator i sekret klienta oraz adres metadanych dostawcy. - Parametry
stateinonce, kluczowe dla bezpieczeństwa przepływu, są obsługiwane automatycznie i nie trzeba ich podawać. - Żadne tokeny nie są domyślnie udostępniane aplikacji. Trzeba je jawnie włączyć, a dokumentacja przy okazji ostrzega, by nie pokazywać ich w interfejsie, logach ani komunikatach błędów i używać wyłącznie do wywołań po stronie serwera.
- Skonfigurowanie uwierzytelniania automatycznie włącza ochronę CORS i XSRF — i ma to pierwszeństwo nad ustawieniami w pliku konfiguracyjnym. Dobra domyślna decyzja, ale zaskoczy kogoś, kto wyłączył je świadomie.
- Ciasteczko tożsamości wygasa po 30 dniach, jeśli użytkownik się nie wyloguje. Nowa karta w tej samej przeglądarce loguje się automatycznie.
- Uwierzytelnianie nie jest obsługiwane w aplikacjach osadzanych w innej stronie. To ograniczenie, nie usterka — i potrafi wywrócić plan wdrożenia panelu w firmowym intranecie.
Dla panelu z danymi osobowymi to zmienia obraz: zamiast tajnego adresu można podłączyć firmowego dostawcę tożsamości i mieć logowanie na tych samych zasadach co reszta systemów, z wylogowaniem i tożsamością dostępną w kodzie. Warto to zrobić przed pierwszym audytem, nie po nim.
Pułapki w jednym miejscu
- Skrypt wykonuje się cały, przy każdej interakcji. Wszystko inne wynika z tego jednego faktu.
- Nie pomyl dwóch pamięci podręcznych. Dane przez
st.cache_data, zasoby przezst.cache_resource— ta druga zwraca ten sam obiekt wszystkim sesjom. - Zmienne lokalne nie przeżywają kliknięcia. Co ma trwać, idzie do stanu sesji.
- Wartości zignorowane przez
on_change="ignore"giną przy odświeżeniu strony, o ile nie powiążesz ich z adresem. - Wiązanie z adresem wymaga klucza, a powiązanego parametru nie zmienisz przez
st.query_params— tylko przez stan sesji. - Wartość widżetu przepada, gdy przestaje być rysowany, dopóki nie ustawisz trwałości stanu.
- Fragmenty równoległe mają zablokowane polecenia — okna dialogowe, przełączanie stron i zapis do kontenerów zewnętrznych — ale tylko w przebiegu równoległym.
- Aby fragment mógł pisać do kontenera zewnętrznego, kontener musi dostać przynajmniej jeden zapis w pierwszym pełnym przebiegu.
- Uwierzytelnianie działa tylko z OIDC i tylko w aplikacji nieosadzanej; wymaga dodatkowej biblioteki.
- Gałąź domyślna to
develop, niemain.
Podsumowanie
Streamlit jest narzędziem o jednej, radykalnej decyzji projektowej i pełnej garści mechanizmów, które tę decyzję czynią znośną. Kto pozna te mechanizmy, dostaje najkrótszą znaną mi drogę od skryptu w Pythonie do rzeczy, którą można pokazać zarządowi. Kto ich nie pozna, będzie walczył z narzędziem i przegra.
- Miarowe wydania — 1.63.0 z września 2026, kolejne co około dwa tygodnie, od siedmiu lat bez przestojów.
- Dwie pamięci podręczne o jasno rozdzielonych rolach, z odświeżaniem w tle w najnowszym wydaniu.
- Fragmenty z odświeżaniem cyklicznym i wykonaniem równoległym dla wolnych, niezależnych operacji.
- Trzy nowe parametry widżetów — ignorowanie zmian, wiązanie z adresem i trwałość stanu — które adresują trzy najczęstsze skargi.
- Logowanie przez firmowego dostawcę tożsamości, z bezpiecznymi domyślnymi ustawieniami i automatyczną ochroną CORS oraz XSRF.
- Opłacany zespół za rozwojem, co przy narzędziu wewnętrznym jest realnym parametrem trwałości.
Licencja to Apache 2.0 i w tym przypadku jest to wybór o konkretnej wartości. Poza swobodą użycia komercyjnego i brakiem obowiązku otwierania własnego kodu daje bowiem wyraźne udzielenie licencji patentowej przez współtwórców — czego licencje typu MIT nie zawierają. Przy projekcie prowadzonym przez dużą firmę technologiczną, mającą własne portfolio patentowe, to nie formalność, a konkretne zabezpieczenie dla twojego działu prawnego. W zamian licencja wymaga zachowania informacji o prawach autorskich, dołączenia pliku z informacjami dodatkowymi oraz oznaczenia wprowadzonych zmian, jeśli rozprowadzasz zmodyfikowaną wersję. Przy zwykłym użyciu — a takie tu przeważa — nie zmienia to nic.
Warto natomiast zachować w pamięci to, czego licencja nie obejmuje: przyszłych decyzji właściciela. Kod już wydany pozostaje wolny bezpowrotnie, ale to, w którą stronę pójdzie rozwój i co znajdzie się w płatnej ofercie chmurowej, jest decyzją firmy. Przy narzędziu do paneli wewnętrznych ryzyko jest niewielkie — najgorszy scenariusz to zatrzymanie się na wersji, którą już masz — ale przy planowaniu dłuższym niż rok wypada je nazwać, zamiast zakładać, że rytm dwutygodniowych wydań utrzyma się sam z siebie.