How to send a push notification from Python
A model finishes training, a nightly job wraps up, a scraper hits the page you were waiting for — and your phone buzzes. In Python that's one HTTP POST to your Lert URL. No SDK to install, no API key to manage. Here's the clean way to wire it up.
What you'll need
Two things: the Lert app on your iPhone, and Python 3.6 or newer. Open the app and copy your endpoint — it looks like https://lert.dev/p/your-id. That URL is the whole authentication story. Anyone who has it can send you a notification, so keep it out of public repos, and regenerate it in the app if it ever leaks.
The examples below use the popular requests library, then show a standard-library urllib version for when you can't add a dependency. Everything here is plain HTTP, so it runs the same whether you're in a Jupyter notebook, a Django management command, a Celery task, or a throwaway script you'll delete in an hour.
One habit worth adopting early: read the endpoint from an environment variable rather than pasting it into the file. It keeps the URL out of version control, and it lets you swap channels — say a noisy dev endpoint versus a quiet production one — without touching code.
The minimal example
Install requests with pip install requests, then send a JSON body. All three fields — title, body, and tag — are optional, so send only what you need.
import requests requests.post( "https://lert.dev/p/your-id", json={"title": "Training done", "body": "epoch 40 · loss 0.11"}, )
Passing json= tells requests to serialize the dict and set Content-Type: application/json for you. Run it and your phone lights up within a second or two — even locked.
A reusable notify() function
You'll want this in more than one place, so wrap it. Put your endpoint in an environment variable rather than hard-coding it, return whether delivery succeeded, and give the timeout a sane value so a network hiccup never hangs your script.
import os, requests LERT_URL = os.environ["LERT_URL"] # https://lert.dev/p/your-id def notify(title, body="", tag=None): payload = {"title": title, "body": body} if tag: payload["tag"] = tag r = requests.post(LERT_URL, json=payload, timeout=10) r.raise_for_status() return r.json().get("delivered", 0) == 1 # usage notify("Backup complete", "42 GB → S3", tag="nightly")
The tag is a free-form label that shows up in your in-app history, so you can tell your nightly backup apart from your deploy pings at a glance.
Handling non-200 responses
On success you get 200 with {"delivered":1}. A few other cases are worth handling explicitly:
- 200 with
{"delivered":0}and a warning — the endpoint is valid but no device is registered yet. Open the Lert app once on your phone to fix it. - 404
{"error":"unknown endpoint"}— the URL is wrong or was regenerated. Copy a fresh one from the app. - 429 — you hit a rate limit (60 requests/minute per IP, or 50/day on the free plan). Respect the
Retry-Afterheader.
import time, requests def notify(title, body="", retries=1): for attempt in range(retries + 1): r = requests.post(LERT_URL, json={"title": title, "body": body}, timeout=10) if r.status_code == 429 and attempt < retries: wait = int(r.headers.get("Retry-After", "5")) time.sleep(wait) continue r.raise_for_status() return r.json() return {"delivered": 0}
Plain text vs JSON
If all you want is a one-line message, skip JSON and send the body as plain text. There's one gotcha: if your text contains = or &, set the content type explicitly so it isn't parsed as form data.
requests.post(
LERT_URL,
data="Queue drained — 0 jobs left".encode(),
headers={"Content-Type": "text/plain"},
)
No dependencies? Use urllib
On a hardened box where you can't pip install, the standard library does the job. urllib.request ships with every Python.
import json, urllib.request def notify(title, body=""): data = json.dumps({"title": title, "body": body}).encode() req = urllib.request.Request( LERT_URL, data=data, headers={"Content-Type": "application/json"}, ) with urllib.request.urlopen(req, timeout=10) as resp: return json.loads(resp.read())
A real-world snippet
Wrap a long job in try/finally so you're notified whether it succeeds or blows up — the notification fires either way.
try: run_expensive_pipeline() notify("Pipeline finished ✅", "all stages green", tag="etl") except Exception as e: notify("Pipeline failed ❌", str(e)[:200], tag="etl") raise
Doing this for a scheduled task? The same pattern shines in a cron job and pairs perfectly with our long-running script and cron notification guides. Prefer another language? See Node.js or Go.
Why it's this simple
Lert deliberately has no client library. The endpoint URL is the credential, so there's nothing to authenticate, no token to refresh, and no SDK version to keep current. That's why "notify my phone from Python" is three lines and not an afternoon.
Because it's just an HTTP call, it composes with everything you already use: put notify() in a shared utils module, call it from an atexit handler, or wire it into your logging so a logger.critical also reaches your pocket. The building block is small on purpose, which is what makes it easy to reach for.
FAQ
Do I need the requests library?
No. requests is the cleanest option, but Python's standard-library urllib.request can send the same POST with zero dependencies — handy on locked-down servers where you can't run pip.
How do I send plain text instead of JSON?
Send the text as the request body with Content-Type: text/plain. This matters when your message contains = or &, which would otherwise be read as form encoding.
What happens if I hit the rate limit?
Lert returns HTTP 429 with a Retry-After header. Read that header, sleep for that many seconds, then retry. The limits are 60 requests/minute per IP and 50/day on the free plan.
Why did I get delivered:0?
A 200 with delivered:0 and a warning means no device is registered for that endpoint yet. Open the Lert app once on your phone to register it, then try again.
Related guides
Give your Python scripts a voice.
Download Lert, copy your URL, drop in a notify() function. Your phone tells you the moment the work is done.