Artemis
A stupidly easy way to build Android and desktop apps in Python.
Artemis sits on top of Flet (which itself sits on Flutter), and trims day-to-day app building down to something that reads almost like pseudocode:
import artemis as art
app = art.App("Hello", theme="ocean")
@app.page("/")
def home(page):
return art.Column([
art.Title("Hello, Artemis"),
art.Button("Say hi", on_click=lambda e: print("hi!")),
], center=True)
app.run()
Run that, and you get a real, native-feeling app - not a wrapped webview -
with a Material 3 color scheme, correct fonts, and rounded modern-looking
controls, for free. Run the exact same file on Android with flet build
apk and it just works, because under the hood it's still Flet/Flutter the
whole way down.
Artemis isn't trying to replace Flet or compete with it - think of it as
the "batteries included" layer on top: opinionated defaults, a much
smaller vocabulary, and routing/state wired up so you rarely touch
page.update() yourself.
Install
cd artemis
pip install artemis-ui
Want the optional bits too (custom logo → .ico auto-conversion, and
charts)?
pip install -e ".[icons,charts]"
(Not published to PyPI yet - this is a first pass at the API. The
distribution name is artemis-ui to avoid clashing with the handful of
other "artemis" packages already on PyPI; you still import artemis.)
Getting ModuleNotFoundError: No module named 'artemis'? That means
Python can't see the package - almost always because you're running a
script from inside examples/ while pip install -e . was never run,
or was run from the wrong folder. Two ways to fix it:
cd artemis
pip install artemis-ui
python examples/counter.py
cd artemis
python examples/counter.py
Either way, run the script from the artemis/ project root (the one
containing both artemis/ and examples/), not from inside Examples/
itself - a script can't import a package that's a sibling of its own
folder rather than its parent.
Being upfront about a genuine Flet limitation: while you're just
running python examples/counter.py (not a real build), the window icon
can only reliably be overridden on Windows, and only via an absolute
path to an actual .ico file - a relative path or a .png gets silently
ignored by Flet's pre-built desktop client (this is a known Flet quirk,
not something Artemis papers over - see flet-dev/flet#3438). Artemis
handles that correctly under the hood: absolute path, real .ico, done.
On macOS, the dock icon during dev preview is baked into Flet's own
pre-built client and literally can't be changed short of building your
own white-labeled Flet client - not an Artemis limitation, a Flet one.
Either way, flet build/flet pack (a real build) get the icon right
on every platform via icon.png above.
Want your own logo? Drop your own logo.png in assets/ and it's
used instead automatically. If Pillow is installed, Artemis also
auto-converts it into a matching logo.ico for you so your own branding
shows up in the Windows dev-preview window icon too, not just ours. If
Pillow isn't installed, Artemis falls back to its own default .ico for
that one narrow case (pip install pillow if you want your PNG
converted automatically) - or just supply your own logo.ico directly
and Artemis will leave it alone.
If you want a different filename or a different assets folder entirely:
app = art.App("My App", logo="brand.png")
app.run(assets_dir="static")
The mental model
- One
App. You make one, give it a title and a theme. - Pages are just functions. Decorate a function with
@app.page("/route")and return whatever control tree that screen should show. - State is a box, not magic.
art.State(0)gives you something you can mutate from inside a closure. Change.valueand the screen updates - no signals, no dependency graph to reason about. - Buttons/switches/sliders auto-redraw. After their
on_click/on_changefires, Artemis quietly re-runs your page function and refreshes the screen. You just change state and move on. - Text inputs are the one exception. Redrawing the whole page on every
keystroke would drop your cursor position, so
Input()takes abind=State instead and updates it silently, no redraw, no dropped focus.
What actually makes this different from plain Flet
Flet gives you the primitives; Artemis makes the everyday app-shell stuff (the part every mobile app needs and every Flet tutorial re-explains from scratch) a one-liner:
- A real navigation stack, not a page swap.
app.go("/details")pushes a properflet.Viewonto the stack with a back arrow that appears on its own. Android's hardware back button and the browser back button both pop it correctly -app.back()does the same thing in code. Plenty of "simple Flet wrapper" libraries just clearpage.controls, which quietly breaks both of those. app.bottom_nav([...])gives you a persistent tab bar across your root screens - tap a tab, the root screen swaps; push into a detail view withapp.go()and the tab bar correctly disappears until you go back, same as it would in a native app.art.Box(glass=True)- an instant frosted-glass panel (blur, faint border, translucent fill). In raw Flet that'sft.Blur+ft.BoxShadow- a manually opacity-adjusted
bgcolor, three separate imports to get one visual effect; here it's a keyword argument.
- a manually opacity-adjusted
art.Box(gradient=["#6366F1", "#EC4899"])- builds theft.LinearGradientfor you instead of you constructing one by hand.app.toast("Saved!")- a one-line snackbar. Nopageargument to pass around, no remembering thatSnackBaris technically a dialog-like control under the hood.
Power features
The stuff above is the everyday layer. This is the stuff that makes Artemis a framework rather than a widget-wrapper library.
Route params
@app.page("/user/:id")
def profile(page, params):
return art.Text(f"User #{params['id']}")
app.go("/user/42")
Your handler only receives params if it actually asks for a second
argument - plain def home(page): handlers keep working untouched.
Responsive layouts (one codebase, phone and desktop)
@app.page("/")
def home(page):
return art.responsive(
page,
mobile=art.Column([...]),
desktop=art.Row([...]),
desktop_at=700,
)
Artemis re-renders the current screen whenever the window resizes, so this picks up live changes as you resize a desktop window - genuinely one codebase behaving differently on a phone versus a desktop window, which is the whole point of an "Android and desktop" library.
Forms with real validation
email = art.Field("", art.validators.required(), art.validators.email())
password = art.Field("", art.validators.required(), art.validators.min_length(6))
form = art.Form(email=email, password=password)
art.Input(label="Email", field=email)
art.Input(label="Password", field=password, password=True)
art.Button("Sign in", on_click=form.submit(lambda values: log_in(values["email"], values["password"])))
form.submit(...) only calls your function if every field passes;
otherwise each Input shows its own error message. Errors stay hidden
until a field's been touched (blurred, or a submit was attempted), so a
fresh form doesn't greet the user with a wall of red text. Built-in
validators: required, email, min_length, max_length, matches
(for "confirm password" fields), number - and they're just functions
that take a value and return an error string or None, so writing your
own is a five-line function.
Persistent state (survives an app restart, no database needed)
theme_pref = art.PersistentState("theme", default="indigo")
theme_pref.value = "forest" # written to disk immediately
Same API as State - it's a drop-in swap - but backed by a small JSON
file in a .artemis_data/ folder next to your script, so it's there the
next time your app starts. Good for remembering a theme choice, the last
tab someone was on, a small local list - not a replacement for a real
database once your data gets complicated.
Charts
art.LineChart([12, 19, 14, 24, 22, 30], labels=["Jan", "Feb", "Mar", "Apr", "May", "Jun"])
art.BarChart([12, 19, 8], labels=["Q1", "Q2", "Q3"])
art.PieChart({"Rent": 1200, "Food": 400, "Fun": 200})
Charts are an optional Flet add-on (pip install flet-charts) with a
fairly verbose raw API - manually built DataPoint objects, hand-wired
axis labels. These three functions take a plain list or dict and build
all of that for you. If flet-charts isn't installed, calling one of
these gives you a clear "pip install flet-charts" message instead of a
confusing import error.
Live theme switching
app.set_theme("forest")
app.set_theme(dark_mode=True)
Handy paired with PersistentState for a real "remember my theme"
settings screen - see examples/dashboard.py.
The artemis CLI
artemis new "My Cool App"
cd "My Cool App"
pip install -e .
python main.py
Scaffolds a starter main.py, pyproject.toml, assets/ folder, and
.gitignore - a real project layout instead of a blank file.
A global error boundary
If a page function raises an exception - a bad route param, a typo in a dict key, anything - Artemis shows a friendly "something went wrong" screen with a "Go home" button instead of crashing the whole app. The real traceback still prints to your console either way; this is just what the user sees instead of a frozen or blank screen. This happens automatically - there's nothing to configure.
Async event handlers (file pickers, network calls, anything that waits)
Any on_click/on_change can be a plain function or an async def -
Flet natively awaits coroutine handlers, Artemis's auto-redraw wrapper
does too, so this just works:
async def load_data(e):
data = await fetch_something()
results.value = data
art.Button("Load", on_click=load_data)
Clipboard
app.copy("https://example.com/shared")
art.Button("Paste", on_click=app.paste(lambda text: print("got:", text)))
A side navigation drawer
app.set_drawer([
{"label": "Home", "icon": art.flet.Icons.HOME, "route": "/"},
{"label": "Settings", "icon": art.flet.Icons.SETTINGS, "route": "/settings"},
])
Same idea as bottom_nav, but a side menu - better suited to a wide
desktop window than a bottom bar, which eats vertical space you don't
have to spare there. Shows a hamburger icon in the AppBar automatically
(standard Flutter behavior once a screen has a drawer attached, not
something Artemis wires up by hand).
Page transitions
app = art.App("My App", transitions="cupertino")
Applies to every app.go() / app.back() push and pop, on every
platform, in one keyword.
Date & time pickers
art.Button("Pick a date", on_click=app.pick_date(lambda d: print(d)))
art.Button("Pick a time", on_click=app.pick_time(lambda t: print(t)))
In-page tabs
art.Tabs([
("Overview", art.Text("...")),
("Settings", art.Column([...])),
])
Not navigation between screens (that's bottom_nav/set_drawer) - just
switching which panel is visible on one screen. Raw Flet's Tabs needs a
separate TabBar + TabBarView kept in sync with a matching length
you update by hand; this is just a list of (label, content) pairs.
Expandable sections
art.Expandable("Shipping details", art.Text("Ships in 2-3 days."))
A collapsible section - FAQs, settings groups, "show more" details.
Badges
art.flet.Icon(art.flet.Icons.NOTIFICATIONS, badge=art.Badge("3", color="rose"))
Not a wrapper - a value you attach to another control's badge
property (most controls have one), the way Flet itself expects it.
Keyboard shortcuts
app.on_key("ctrl+s", lambda e: save())
app.on_key("escape", lambda e: app.back())
A global shortcut - handy for desktop apps. Modifiers are optional and
order doesn't matter ("shift+ctrl+n" and "ctrl+shift+n" both match).
Grid layout
art.Grid([art.Card(art.Text(p)) for p in products], columns=3)
art.Grid(tiles, min_item_width=160)
Network calls
async def load(e):
items.value = await art.fetch_json("https://api.example.com/items")
art.Button("Refresh", on_click=load)
fetch_json, fetch_text, and post_json - thin async wrappers around
httpx (already a Flet dependency, so this adds nothing new to
install), with a timeout and raise_for_status handled for you.
Buttons with an automatic loading spinner
saving = art.State(False)
art.Button("Save", on_click=save_handler, loading=saving)
Pass a State via loading= and the button shows a small spinner
instead of its label while an async on_click is running, and disables
itself so it can't be double-tapped. Artemis flips saving.value for
you before and after - read it elsewhere on the page too if you want a
loading indicator to show up in more than one place at once.
Route guards
app.page("/admin", guard=lambda: current_user.is_admin, redirect="/login")
guard is a zero-arg function checked right before that screen renders;
return False and Artemis shows redirect instead. Saves repeating an
auth check in every protected page function.
Loading data without the boilerplate
Every screen that loads data ends up needing the same three things - a
loading flag, an error slot, a value slot - and since Artemis re-runs
your page function on every click anywhere in the app, you also have
to make sure you don't accidentally re-fetch on every single one of
those re-renders. AsyncData handles all of it:
products = art.AsyncData(lambda: art.fetch_json(URL))
@app.page("/")
def home(page):
products.render(page) # fetches once, no-ops on every re-render after
if products.loading:
return art.Loader()
if products.error:
return art.Text(f"Couldn't load that: {products.error}")
return art.Column([art.Text(p) for p in products.value])
Call products.reset() (e.g. from a "refresh" button) to force the next
render() to fetch again.
Toasts with an action button
app.toast("Task deleted", action="Undo", on_action=lambda e: restore(task))
Testing your app without opening a window
from artemis.testing import TestApp
def test_counter():
t = TestApp(app).build()
assert t.has_text("0")
t.click(t.find_button("+"))
assert t.has_text("1")
TestApp gives your App a fake page (views, dialogs, clipboard, all
of it) so your actual page functions and event handlers run for real,
no Flet client required - useful in CI, and just as useful while you're
building the thing so you're not re-clicking through the UI by hand
after every change. See tests/test_examples.py for a full pytest
suite against two of the example apps - run it with pytest tests/.
What's in the box right now
Text, Title, Button, Input, Switch, Checkbox, Slider,
Dropdown, Column, Row, Grid, Tabs, Expandable, Badge, Box,
Card, Spacer, Divider, Image, BottomNav, ListTile, Avatar,
Loader, ProgressBar, toast, plus App, State, PersistentState,
Field, Form, validators, responsive, and
LineChart/BarChart/PieChart. App also gives you app.alert(...),
app.confirm(...), app.set_theme(...), app.set_drawer(...),
app.copy(...)/app.paste(...), app.pick_date(...)/app.pick_time(...),
and app.on_key(...). Plus art.fetch_json/fetch_text/post_json for
network calls, art.AsyncData for the load/loading/error pattern,
Button(loading=...) for automatic spinners, toast(action=...), route
guards, and artemis.testing.TestApp for testing without a window.
That's genuinely most of what a typical small-to-medium app needs.
Anything Artemis doesn't wrap yet, you can still reach for - import
flet (or art.flet) is right there, and every Artemis widget returns a
plain Flet control, so mixing the two is completely fine.
Themes
31 named palettes ship out of the box, grouped roughly by mood:
- cool:
indigo,ocean,sky,cobalt,royal,teal,cyan,slate,steel,midnight,graphite - green:
forest,mint,emerald,lime,olive - warm:
sunset,rose,cherry,crimson,amber,gold,orange,coral,clay,sand - bold:
grape,violet,magenta,fuchsia,plum
Or skip the list entirely and pass any hex string of your own, e.g.
theme="#22D3EE". Each one is a Material 3 "seed color", so light mode,
dark mode, hover states, and text contrast are all derived automatically
- that's what makes a two-line Artemis app not look like default grey
Flet.
App(..., dark_mode=True/False)forces a mode; leave it out and Artemis follows the system setting.
Full manual control - background, surface, text, and accent colors
For anyone who wants an exact look rather than a named vibe, four more keywords give you direct control over the actual colors, not just the seed:
app = art.App(
"My App",
theme="indigo", # still drives whatever you don't override below
background="#0B1220", # the color behind everything
surface="#161F32", # the color of cards/boxes on that background
text="#E5E7EB", # the default foreground/text color
primary="#818CF8", # the accent color for buttons, switches, etc.
)
Each one you set becomes a fixed, absolute color - in both light and
dark mode, since that's the point of overriding it - while anything you
leave as None still comes from the seed color, light/dark included.
This isn't an Artemis trick sitting on top of Flet; it's exactly how
Flet's own ColorScheme overriding works (color_scheme_seed fills in
the palette, individual ColorScheme fields you set take precedence) -
Artemis just gives you four plain keywords instead of making you build a
ColorScheme object by hand.
All of the above also works live, mid-app, via app.set_theme(...) -
same keywords, and anything you don't pass keeps its current value:
app.set_theme("forest") # just change the palette
app.set_theme(primary="#EF4444") # just nudge the accent color
app.set_theme(background=None, surface=None, text=None) # clear overrides, back to pure seed
See examples/theming.py for all of this side by side.
Examples
examples/counter.py- the classic, shows State + auto-redrawexamples/todo.py- a small task list, showsInput(bind=...), dynamic lists of Cards, and Checkbox togglingexamples/theming.py- named palettes vs full manual background / surface / text / primary control, switchable liveexamples/showcase.py- navigation (app.go/app.back), a bottom tab bar, glass + gradient panels, and a toastexamples/contacts.py-ListTile+Avatarrows,Loader/ProgressBar, andapp.alert()/app.confirm()dialogsexamples/login.py-Field+Form+validatorsfor real form validationexamples/dashboard.py- route params, charts, a responsive layout, and a theme preference that survives an app restartexamples/drawer_and_clipboard.py- a side navigation drawer, async event handlers, clipboard, a Grid layout, and a custom page transitionexamples/tabs_and_shortcuts.py- in-page Tabs, Expandable sections, a Badge, date/time pickers, and a global Ctrl+S keyboard shortcutexamples/network_and_guards.py- a realart.fetch_jsoncall, an automatic loading spinner, and a route guard/redirectexamples/async_data_demo.py-art.AsyncData(load once, not on every re-render) andtoast()with an Undo action button
python examples/counter.py
Shipping to Android / desktop
This is the part Artemis doesn't try to reinvent - it's just Flet, so Flet's own build tooling applies unchanged:
flet build apk
flet build ipa
flet build macos
flet build windows
flet build linux
Known rough edges (being upfront about it)
- Every button/switch click re-runs the current screen's function and redraws it from scratch. It's simple and fast enough for typical apps, but if you're rendering something genuinely huge (thousands of rows) you'll feel it. Scoped/partial updates are on the list for a v2.
bottom_nav()assumes a single level of tabs; nesting a second tab bar inside a pushed screen isn't supported.PersistentStateis plain JSON on disk next to your script - fine for small preferences and lists, not a real database once your data gets relational or large.- Charts need the optional
flet-chartspackage installed separately (pip install flet-chartsorpip install -e ".[charts]"); Artemis doesn't force it on projects that don't need charts. - No file/save/folder picker. There was one; it got pulled. Flet's
underlying
FilePickercontrol has a real, still-open registration bug (Unknown control: FilePicker,flet-dev/flet#6040/#6251/#6422) that's especially bad on Python 3.14, and no amount of retrying or delaying from the Python side reliably fixed it across every environment it was tested against. Rather than ship something that works on some machines and silently fails on others, Artemis doesn't wrap it right now. If you need file access,art.flet's rawFilePickeris still there if you want to experiment with it yourself- just know you're on the same ground Artemis pulled back from.
- Widget coverage is intentionally focused on the common cases, not
exhaustive. Anything Artemis doesn't wrap yet is one
import fletaway - every Artemis widget returns a plain Flet control.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file artemis_ui-0.2.1.tar.gz.
File metadata
- Download URL: artemis_ui-0.2.1.tar.gz
- Upload date:
- Size: 97.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ec2703cfd1c3c6ba3f641a2a6f9d9acdb45f76d497de90ca01c7ca381a28472a
|
|
| MD5 |
788681987cd589b5b0577a90cc04fad4
|
|
| BLAKE2b-256 |
cc4f6f6bbedc2d2c47ae76c716825116eb9df095cfd29317188285a00ab0c070
|
File details
Details for the file artemis_ui-0.2.1-py3-none-any.whl.
File metadata
- Download URL: artemis_ui-0.2.1-py3-none-any.whl
- Upload date:
- Size: 89.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f2e08cccb1e36876348e975d736c43a9d38c0cf851c9cb44b94dad77a6cfbb24
|
|
| MD5 |
c2e35869631072b49697bf0ea4dd07dd
|
|
| BLAKE2b-256 |
451eb605b5e39e075fc699293125e733902778f397456f0802217551f7efa8b3
|