native AI syntax for Python
plum
AI model calls as
native Python expressions — results
come back as real
bool,
int, or
str. No string
parsing.
Runs locally with Ollama. No API keys. No setup.
our belief
AI operations should come as natural as 1+1
That's what plum is built for.
the language
One operator. Real Python values.
The ?[...] operator
embeds a model call anywhere a value fits. The
-> Type hint coerces
the result — builtins or your own classes.
use llama3.2
# the prompt is just a string. trailing ? sends it to the model.
answer = "What is the capital of France?"?
# inject runtime values with f-string syntax
summary = f"Summarize this article: {article}"?
use llama3.2
# -> bool returns an actual Python bool — not a string to parse
if f"Is this email spam? Answer true or false: {email}"? -> bool:
quarantine(email)
# works in list comprehensions, map, filter — anywhere an expression fits
tags = [f"Classify: {p}"? for p in posts]
use llama3.2
ticket = "Production database unreachable, 500 errors on all endpoints"
# -> int is a real int — do math, compare, index, anything
urgency = f"Rate urgency 1-5, number only: {ticket}"? -> int
if urgency >= 4:
page_oncall()
# -> float for continuous scores
sentiment = f"Sentiment score -1.0 to 1.0, number only: {ticket}"? -> float
print(f"urgency={urgency}, sentiment={sentiment:.2f}")
use llama3.2
from dataclasses import dataclass
@dataclass
class Ticket:
urgency: int
category: str
# -> Ticket works with your own classes, not just builtins
t = f"Extract urgency (1-5) and category: {text}"? -> Ticket
print(t.urgency, t.category)
model routing
One pipe routes the model.
The pipe | routes the
prompt. Config lives in one file.
01
Local models, no API keys
Alpha runs on Ollama — no API keys, no internet
required. The
?[...] syntax is
the same regardless of model.
use llama3.2 # file-level default
"..."? # uses llama3.2
?["..." | mistral] # override for this call
?["..." | gemma3] # override for this call
02
Simple config
Alpha runs on Ollama locally — no keys, no config.
The plum.toml file lands right after
v1, bringing full multi-provider support.
# plum.toml
[models]
claude = "claude-3-5-sonnet"
03
Caching built in
Architecture is in place. Identical prompts return cached results. Not shipped yet.
@cache
f"Translate: {text}"?
before & after
Without plum vs. with plum.
Same result. Less noise.
import ollama
response = ollama.chat(
model="llama3.2",
messages=[{
"role": "user",
"content": "Translate to French: " + text
}]
)
result = response["message"]["content"]
use llama3.2
result = f"Translate to French: {text}"?
real programs
Plum in practice.
Programs you can run today. Copy, save as
.plum, run with
plum file.plum.
# spam.plum
use llama3.2 | mistral
emails = [
"Click here to win a free iPhone!!!",
"Hey, are we still on for lunch tomorrow?",
"URGENT: your account has been compromised, click here",
]
for email in emails:
is_spam = f"Is this email spam? Answer only 'true' or 'false':\n\n{email}"? -> bool
label = "SPAM" if is_spam else "ok"
print(f"[{label}] {email[:50]}")
# support.plum
use llama3.2
def handle_ticket(ticket):
urgency = f"Rate urgency 1-5, number only: {ticket}"? -> int
category = f"Classify as billing/shipping/other: {ticket}"?
if urgency >= 4:
escalate(ticket)
return
draft = f"Write a warm reply to this {category} issue: {ticket}"?
send(draft)
questions & answers
Questions we keep getting.
Plum is a Python superset with native AI syntax
— built into the language, not imported as a
library.
Instead of writing 10+ lines
of API boilerplate every time you want to call a
model, you write one line:
use llama3.2
result = f"Summarize this: {text}"?
print(). That's the whole idea.
Depends on your definition — and that's a fair
debate.
Technically: Plum has its own
syntax, transpiles to Python (like CoffeeScript
to JavaScript, TypeScript to JavaScript), and
adds semantics Python doesn't have. By that
measure, it qualifies.
Practically:
it's a Python superset with a new operator. If
that makes it a DSL in your book, that's a
reasonable take too.
What it's
definitely not: a library. You don't import
Plum. You write in it.
Two groups:
Developers
who reach for a bash script or quick Python file
— not an SDK. If you want AI in that script
without setting up LangChain, Plum is for
you.
Non-technical people
comfortable with basic Python who hit a wall the
moment an API is involved. Plum removes that
wall.
Note: the examples on this page
use Python features like list comprehensions and
dataclasses. If you're completely new to
programming, Plum still has a learning curve —
just a shorter one.
? conflict with Python
syntax?
▼
? does not exist anywhere in
Python. It's not an operator, not a reserved
word, not used in any special syntax. Python has
zero use for it.
Compare to
JavaScript where ? is taken for
ternary operators and optional chaining. Or Rust
where it's the error propagation operator.
Python has none of that.
No
ambiguity. No conflict.
Yes, with one rule:
the entry point must be Plum, not
Python.
Same rule as TypeScript — you don't
run .ts files with Node directly.
You use ts-node. Same here:
Your .plum files can import any
.py file freely. Your entire
existing Python codebase works inside Plum
untouched.
Models are listed in priority order separated by
|. Plum tries the first one. If it
fails — rate limit, downtime, error — it
automatically tries the next:
result = ?["do this" | llama3.2 | mistral | gemma3]
# tries llama3.2 → if fails, tries mistral → if fails, tries gemma3
You write this once. Plum handles the retry logic, the error handling, the fallback. Zero extra code from you.
LangChain is a library with an API to learn,
objects to chain, and dependencies to install.
It's built for complex AI pipelines.
Plum
is built for the opposite case: you have a
script, you want one AI call, you don't want to
set anything up.
They're not
competing for the same moment.
Fair point. Here's the honest answer:
AI
writes the boilerplate once.
That code runs thousands of times, gets read,
maintained, and debugged by people who didn't
write it.
Plum makes that code
shorter and clearer — not just to write, but to
read six months later.
That said: if
AI tooling keeps improving, this advantage may
shrink. We're betting on simplicity having
lasting value. Time will tell.
This is a real question. Here's the reframe that
resolves it:
If your problem has a deterministic answer,
you don't need AI.
You need a formula or a lookup. Use normal
code.
When you reach for
?[...] you've already accepted that
the answer requires judgment. You're solving a
non-deterministic problem by definition.
So
you don't test that output is identical every
run. You test that output is
acceptable every run. That's a
different kind of test — and arguably more
honest about what software should do anyway.
Non-determinism
in Plum isn't a bug. It's the whole reason
you're using it.
Real risk. Plum's plan: warn at compile time
when ?[...] appears inside a loop.
Not shipped yet — the warning will look like
this:
Your Plum code doesn't change. The runtime
adapter does.claude in
Plum is a model identifier, not an API version.
When a provider changes their API, only the
adapter gets updated. Your
?[...] expressions stay
identical.
Same reason you don't
rewrite your Python app when Python releases a
new version. The abstraction absorbs the change.
Python is the right choice for v1. Not
necessarily forever.
Why Python now:
Entire AI ecosystem already exists. Fastest path
to working prototype. Largest audience.
Non-technical users already learn Python.
Where Python falls short later:
Slow. GIL limits true parallelism. Not
systems-level.
The decision for v2
gets made with evidence from real users — not
theory. Right now the idea is what matters. Not
the implementation language.
beta
Shape plum with us.
The language is in beta. Try it now and help decide what plum becomes.