Skip to main content

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 .value and the screen updates - no signals, no dependency graph to reason about.
  • Buttons/switches/sliders auto-redraw. After their on_click / on_change fires, 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 a bind= 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 proper flet.View onto 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 clear page.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 with app.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's ft.Blur + ft.BoxShadow
    • a manually opacity-adjusted bgcolor, three separate imports to get one visual effect; here it's a keyword argument.
  • art.Box(gradient=["#6366F1", "#EC4899"]) - builds the ft.LinearGradient for you instead of you constructing one by hand.
  • app.toast("Saved!") - a one-line snackbar. No page argument to pass around, no remembering that SnackBar is 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-redraw
  • examples/todo.py - a small task list, shows Input(bind=...), dynamic lists of Cards, and Checkbox toggling
  • examples/theming.py - named palettes vs full manual background / surface / text / primary control, switchable live
  • examples/showcase.py - navigation (app.go/app.back), a bottom tab bar, glass + gradient panels, and a toast
  • examples/contacts.py - ListTile + Avatar rows, Loader / ProgressBar, and app.alert() / app.confirm() dialogs
  • examples/login.py - Field + Form + validators for real form validation
  • examples/dashboard.py - route params, charts, a responsive layout, and a theme preference that survives an app restart
  • examples/drawer_and_clipboard.py - a side navigation drawer, async event handlers, clipboard, a Grid layout, and a custom page transition
  • examples/tabs_and_shortcuts.py - in-page Tabs, Expandable sections, a Badge, date/time pickers, and a global Ctrl+S keyboard shortcut
  • examples/network_and_guards.py - a real art.fetch_json call, an automatic loading spinner, and a route guard/redirect
  • examples/async_data_demo.py - art.AsyncData (load once, not on every re-render) and toast() 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.
  • PersistentState is 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-charts package installed separately (pip install flet-charts or pip 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 FilePicker control 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 raw FilePicker is 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 flet away - 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

artemis_ui-0.2.1.tar.gz (97.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

artemis_ui-0.2.1-py3-none-any.whl (89.5 kB view details)

Uploaded Python 3

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

Hashes for artemis_ui-0.2.1.tar.gz
Algorithm Hash digest
SHA256 ec2703cfd1c3c6ba3f641a2a6f9d9acdb45f76d497de90ca01c7ca381a28472a
MD5 788681987cd589b5b0577a90cc04fad4
BLAKE2b-256 cc4f6f6bbedc2d2c47ae76c716825116eb9df095cfd29317188285a00ab0c070

See more details on using hashes here.

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

Hashes for artemis_ui-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 f2e08cccb1e36876348e975d736c43a9d38c0cf851c9cb44b94dad77a6cfbb24
MD5 c2e35869631072b49697bf0ea4dd07dd
BLAKE2b-256 451eb605b5e39e075fc699293125e733902778f397456f0802217551f7efa8b3

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page