Email alerts are great — until they sit unread under 400 promotional emails. Your price tracker sends you a price drop at 8 AM, and you see it at 11 PM.
Telegram notifications are different. They land on your phone like a text message: a ping, a preview, and you look now. And sending them from Python takes about 10 lines, no library required — just requests, which you already know from the web scraping guide.
By the end of this guide you'll have a send_telegram() function you can drop into any script: backup finished, price dropped, server hiccuped, download done.
Step 1: Create Your Bot (2 Minutes)
You create a bot by talking to another bot. Open Telegram, search for @BotFather (the official bot manager), and:
- Send
/newbot - Answer the name prompt — anything, e.g.
My Server Alerts - Answer the username prompt — must end in
bot, e.g.myserver_alerts_bot - BotFather replies with your token, which looks like:
123456789:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw
Treat that token like a password. Anyone who has it can send messages as your bot. Store it in an environment variable:
export TELEGRAM_BOT_TOKEN="123456789:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw"
Step 2: Get Your Chat ID
Your bot can't message you until it knows where to message. Telegram identifies every chat by a numeric ID, and there's a chicken-and-egg problem: you need to talk to the bot first.
Open your bot's chat in Telegram (BotFather's reply links to it) and press Start, or just send it a message — anything, hi works. Now the bot has a record of your chat. Ask Telegram for it:
import requests
token = "123456789:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw"
resp = requests.get(f"https://api.telegram.org/bot{token}/getUpdates", timeout=10)
updates = resp.json()
for update in updates["result"]:
print(update["message"]["chat"]["id"])
12345678
That number is your chat ID. Save it too:
export TELEGRAM_CHAT_ID="12345678"
Step 3: The 10-Line Notification Function
Telegram's API is a plain HTTP API — send a POST request to sendMessage and the message appears on your phone. That's why you don't need a Telegram library:
import os
import requests
BOT_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
CHAT_ID = os.environ["TELEGRAM_CHAT_ID"]
def send_telegram(text):
"""Send a message to your phone via Telegram."""
url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage"
resp = requests.post(
url,
json={"chat_id": CHAT_ID, "text": text},
timeout=10,
)
resp.raise_for_status() # crash loudly if Telegram rejects the message
return resp.json()
send_telegram("Hello from Python! 🤖")
Run it, glance at your phone. A real message, sent by your own code. That feeling doesn't get old.
(The requests.post(..., json=...) pattern is the same one you'd use for any modern API — we use it throughout the web scraping guide and beyond.)
Formatting: Bold, Emoji, and Multi-Line
Add parse_mode and you get Markdown formatting for free:
def send_telegram(text):
url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage"
resp = requests.post(
url,
json={
"chat_id": CHAT_ID,
"text": text,
"parse_mode": "Markdown",
},
timeout=10,
)
resp.raise_for_status()
return resp.json()
send_telegram("*Backup finished* ✅\n\n- 240 files copied\n- 0 errors\n- took 3m 12s")
*bold*, _italic_, and \n line breaks all work. Watch out: if your message contains a stray * or _, Telegram throws a parse error. For messages built from unpredictable data (filenames, error logs), skip parse_mode — plain text never breaks.
Make It Reusable
You'll want send_telegram() in every script from now on. Rather than copying it each time, save it once as a module:
"""notify.py — import this in any script that needs phone alerts."""
import os
import requests
BOT_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
CHAT_ID = os.environ["TELEGRAM_CHAT_ID"]
def send_telegram(text):
url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage"
resp = requests.post(url, json={"chat_id": CHAT_ID, "text": text}, timeout=10)
resp.raise_for_status()
Then any script, anywhere on your machine, gets phone notifications with two lines:
from notify import send_telegram
send_telegram("Something happened and you should know.")
Keep notify.py in the same folder as your scripts, or better, in a ~/scripts/ folder you add to PYTHONPATH — one copy of the function, one place to fix if Telegram changes anything.
Sending Photos and Files
Text is the 90% case, but the API also sends photos, documents, and any other file. The pattern is the same — a POST with a files= argument:
def send_photo(photo_path, caption=""):
"""Send a photo (PNG/JPG) to your phone."""
url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendPhoto"
with open(photo_path, "rb") as photo:
resp = requests.post(
url,
data={"chat_id": CHAT_ID, "caption": caption},
files={"photo": photo},
timeout=30,
)
resp.raise_for_status()
Where does a script get a photo? Charts, mostly. If your price tracker keeps a history list instead of just last_price, a matplotlib chart of the last 30 days is one savefig() away — and now it can be one send_photo() away from your phone:
send_photo("price-chart.png", caption="📈 Coffee maker — last 30 days")
For other files, swap the endpoint to sendDocument and the field name to document. Same idea, any file type.
Reading Replies (Optional)
So far the messages flow one way: Python → you. What about you → Python? The getUpdates endpoint from Step 2 also does long-polling — the API holds your request open until a message arrives:
offset = 0
while True:
resp = requests.get(
f"https://api.telegram.org/bot{BOT_TOKEN}/getUpdates",
params={"offset": offset, "timeout": 30},
timeout=35,
)
for update in resp.json()["result"]:
offset = update["update_id"] + 1 # consume it so we don't read it twice
text = update["message"]["text"]
print(f"You said: {text}")
Two details do the heavy lifting: timeout=30 tells Telegram to wait up to 30 seconds for new messages (instead of answering instantly with nothing), and offset marks each update as consumed so the next poll doesn't replay the whole history.
This loop is the seed of a conversational bot — one that answers commands. A price tracker you can text /check coffee maker and get a reply. That's a fun next step, but it comes with concurrency, state, and retry logic you don't want to hand-roll — for that project, reach for the python-telegram-bot library instead.
Real Project: The Price Tracker, Now on Your Phone
This is where the pieces click together. Our price tracker emails you when a product drops. Swap the email function for send_telegram() and alerts hit your phone instantly. Want a finished example with a live news source? My Python sports alert bot pushes NBA headlines to Telegram every morning:
"""price-tracker-telegram.py — price tracker with phone notifications."""
import os
import requests
from price_tracker_helpers import get_price, load_history, save_history # from the price tracker guide
BOT_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
CHAT_ID = os.environ["TELEGRAM_CHAT_ID"]
def send_telegram(text):
url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage"
resp = requests.post(url, json={"chat_id": CHAT_ID, "text": text}, timeout=10)
resp.raise_for_status()
PRODUCTS = {
"coffee maker": {
"url": "https://shop.example.com/coffee-maker",
"selector": "span.current-price",
},
}
history = load_history()
for product, cfg in PRODUCTS.items():
try:
current = get_price(cfg["url"], cfg["selector"])
except Exception as e:
send_telegram(f"⚠ Price check failed for {product}: {e}")
continue
previous = history.get(product, {}).get("last_price")
if previous is not None and current < previous:
pct = (previous - current) / previous * 100
send_telegram(
f"💰 *Price drop!* {product}\n"
f"${previous:.2f} → ${current:.2f} ({pct:.0f}% off)"
)
history[product] = {"last_price": current}
save_history(history)
Notice the failure alert too — when the scraper breaks, you find out immediately instead of silently missing deals for a week.
The same pattern upgrades any script. Here's the backup automation example, reporting in both directions:
import subprocess
result = subprocess.run(["/home/user/backup.sh"], capture_output=True, text=True)
if result.returncode == 0:
send_telegram("✅ Backup completed without errors")
else:
send_telegram(f"❌ Backup FAILED:\n{result.stderr[:200]}")
Success messages feel like noise at first. Keep them — a bot that only ever reports bad news makes you wonder whether it's still running.
The Fine Print: What Bots Can't Do
- Bots can't message you first. You must press Start in the bot's chat before it can send you anything. That's why Step 2 exists.
- Rate limits. Telegram allows ~30 messages per second globally per bot, but only ~20 per minute to a single chat. For personal alerts you'll never get close — unless you put
send_telegram()inside a loop over 10,000 files. - One-way by design. A notification bot doesn't reply to you. If you want a bot that answers commands (
/status,/pause), that's a bigger project — look into thepython-telegram-botlibrary, which handles interactive conversations. - Token hygiene. Committed your token to GitHub? Assume it's stolen. Revoke it immediately via BotFather (
/revoke) and generate a new one. Environment variables exist for exactly this reason.
Common Problems (And Their Fixes)
| Symptom | Cause | Fix |
|---|---|---|
401 Unauthorized |
Wrong token | Copy it again from BotFather (/mybots → API Token) |
400 Bad Request: chat not found |
You never pressed Start | Open your bot's chat → Start → re-run Step 2 |
429 Too Many Requests |
Hit a rate limit | Add a time.sleep(1) between messages |
400 Bad Request: can't parse entities |
Stray * or _ with Markdown enabled |
Drop parse_mode for unpredictable text |
| Message never arrives | Wrong chat ID | Re-run the getUpdates snippet from Step 2 |
What's Next?
You now have two notification channels: email for reports you read later, Telegram for alerts you need now. A good rule of thumb: email the daily digest, Telegram the emergency.
Put your new function to work:
- Price drops on your phone — the project above, scheduled with cron
- Backup pass/fail reports — the Python backups guide plus the snippet above
- "Someone emailed me" alerts — combine with reading your inbox to get pinged when a specific sender writes
Start with the Hello World message, then wire it into one script you run regularly. Your phone buzzing because your code decided to tell you something — that's the whole point of automation.
All code in this article was tested and runs successfully on Python 3.8.10 (Ubuntu 20.04) with requests 2.32.4 — verified August 2026.