Docs › Discord bots › Course
Part 4: Save data with SQLite (points and leaderboard)
Part 4 of the Python Discord bot course: store data in SQLite with aiosqlite and build /daily, /points and /leaderboard commands that survive restarts.
- Part 1: Create the bot account
- Part 2: Hello world with a /ping command
- Part 3: Real commands, embeds, buttons and cogs
- Part 4: Save data with SQLite (you are here)
- Part 5: Keep it online 24/7 on ESN
Variables vanish when the bot restarts. To remember things, like points, warnings or settings, save them to a database. SQLite is a database in a single file, and Python already knows how to use it.
Why aiosqlite? #
Python's built-in sqlite3 blocks while it reads or writes. In a bot, blocking code freezes everything, including the heartbeat that keeps it connected (see bot keeps disconnecting). aiosqlite wraps the same database in async functions you can await.
1. Install it #
python -m pip install -U aiosqlite
2. Write cogs/points.py #
import aiosqlite
import discord
from discord import app_commands
from discord.ext import commands
DB_PATH = "data.db" # created next to bot.py the first time the bot starts
class Points(commands.Cog):
def __init__(self, bot: commands.Bot) -> None:
self.bot = bot
self.db: aiosqlite.Connection | None = None
async def cog_load(self) -> None:
self.db = await aiosqlite.connect(DB_PATH)
await self.db.execute(
"""
CREATE TABLE IF NOT EXISTS points (
guild_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
points INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (guild_id, user_id)
)
"""
)
await self.db.commit()
async def cog_unload(self) -> None:
if self.db:
await self.db.close()
async def get_points(self, guild_id: int, user_id: int) -> int:
async with self.db.execute(
"SELECT points FROM points WHERE guild_id = ? AND user_id = ?", (guild_id, user_id)
) as cursor:
row = await cursor.fetchone()
return row[0] if row else 0
@app_commands.command(name="daily", description="Claim 10 points once a day")
@app_commands.guild_only()
@app_commands.checks.cooldown(1, 86400, key=lambda i: (i.guild_id, i.user.id))
async def daily(self, interaction: discord.Interaction) -> None:
await self.db.execute(
"""
INSERT INTO points (guild_id, user_id, points) VALUES (?, ?, 10)
ON CONFLICT (guild_id, user_id) DO UPDATE SET points = points + 10
""",
(interaction.guild_id, interaction.user.id),
)
await self.db.commit()
total = await self.get_points(interaction.guild_id, interaction.user.id)
await interaction.response.send_message(f"+10 points! You now have **{total}**.")
@app_commands.command(name="points", description="See how many points someone has")
@app_commands.guild_only()
async def points(self, interaction: discord.Interaction, member: discord.Member | None = None) -> None:
member = member or interaction.user
total = await self.get_points(interaction.guild_id, member.id)
await interaction.response.send_message(f"{member.display_name} has **{total}** points.")
@app_commands.command(name="leaderboard", description="Top 10 members by points")
@app_commands.guild_only()
async def leaderboard(self, interaction: discord.Interaction) -> None:
async with self.db.execute(
"SELECT user_id, points FROM points WHERE guild_id = ? ORDER BY points DESC LIMIT 10",
(interaction.guild_id,),
) as cursor:
rows = await cursor.fetchall()
if not rows:
await interaction.response.send_message("No points yet. Try /daily!")
return
lines = [f"**{n}.** <@{user_id}>: {pts}" for n, (user_id, pts) in enumerate(rows, start=1)]
embed = discord.Embed(title="Leaderboard", description="\n".join(lines), color=discord.Color.gold())
await interaction.response.send_message(embed=embed)
async def setup(bot: commands.Bot) -> None:
await bot.add_cog(Points(bot))cog_loadruns when the cog loads: it opensdata.db(creating it if needed) and makes the table.- The table's key is
(guild_id, user_id), so each server has its own points. INSERT ... ON CONFLICT ... DO UPDATEadds 10 points, creating the row the first time.- Always use
?placeholders, never f-strings, to put values in SQL. That blocks SQL injection. <@user_id>in an embed shows the member's name without pinging them.@app_commands.guild_only()hides these commands in DMs, where there's no server to count points for.
3. Load it #
In bot.py, add the new cog to the list:
EXTENSIONS = ["cogs.fun", "cogs.points"]
4. Test it #
Run python bot.py. Use /daily, then /points and /leaderboard. Stop the bot with Ctrl+C, start it again and check /points: the number is still there, because it's saved in data.db.
/daily cooldown lives in memory, so it resets when the bot restarts. For a strict once-a-day rule, save the last claim time in the database too. That's a good exercise.Settings and secrets #
Keep secrets such as the token in .env, never in code or in the database. Your project now looks like this:
my-bot/
├── .env
├── bot.py
├── data.db (created on first run)
└── cogs/
├── fun.py
└── points.pyWrite down your packages for the server in Part 5. Create requirements.txt:
discord.py
python-dotenv
aiosqlite
FAQ #
Where is data.db?
In the folder you run the bot from, next to bot.py. On ESN Hosting it's in your server's top folder in Files.
Can several servers use the bot at once?
Yes. Every row stores the guild_id, so each server has its own points and leaderboard.
When should I use PostgreSQL instead?
SQLite is plenty for one bot in up to thousands of servers. Move to PostgreSQL if you run several bot processes (shards on different machines) that must share data.
Still stuck? Use the live chat on the panel or ask in the ESN Systems Discord. Last updated November 3, 2026.