pythonpython-3.xdiscorddiscord.py

How Do I Find Members Not In my server? (d.py)


I've been attempting to solve an issue I have in my server's economy where the leaderboard still shows stats of players who left. To this end, I am trying to create a clean_economy command, where the bot automatically removes the json information from players no longer in the server. (Yes, I know alternatives to JSON exist, but I only use this bot for one server, and it's relatively lightweight in terms of users so far.)

Here is the code I currently have. Note that it is in a cog, and all intents are enabled:

import discord
from discord.ext import commands
from discord import app_commands
import json
import random
import time
import asyncio
import datetime
from typing import Literal
import datetime
from datetime import timedelta

...

    @commands.hybrid_command(name="clean_economy", description="Removes the people who are no longer in the server from the economy module.")
    @commands.has_role("*")
    async def clean_economy(self, ctx: commands.Context) -> None:
      with open("cogs/bank.json", "r") as f:
        bank = json.load(f)
      await ctx.send("Are you SURE?")
      def check(m):
        return m.author == ctx.author and m.channel == ctx.channel
      msg = await self.client.wait_for("message", check=check, timeout=60.0)
      if msg.content.lower() == "yes":
        await ctx.send("Cleaning economy...")
        await asyncio.sleep(2.0)
        users_to_remove = []
        for user_id, user_data in bank.items():
          member = ctx.guild.get_member(user_id)
          if member is None:
            users_to_remove.append(user_id)
          for user_id in users_to_remove:
            del bank[user_id]
        with open("cogs/bank.json", "w") as f:
          json.dump(bank, f)
        await ctx.send("Done!")
      else:
        await ctx.send("Cancelled!")

...

The error I keep getting is:

Traceback (most recent call last):
  File "/home/runner/Capitol-Hill-Bot-1/.pythonlibs/lib/python3.10/site-packages/discord/ext/commands/core.py", line 235, in wrapped
    ret = await coro(*args, **kwargs)
  File "/home/runner/Capitol-Hill-Bot-1/cogs/economy.py", line 984, in clean_economy
    for user_id, user_data in bank.items():
RuntimeError: dictionary changed size during iteration

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/runner/Capitol-Hill-Bot-1/.pythonlibs/lib/python3.10/site-packages/discord/ext/commands/bot.py", line 1350, in invoke
    await ctx.command.invoke(ctx)
  File "/home/runner/Capitol-Hill-Bot-1/.pythonlibs/lib/python3.10/site-packages/discord/ext/commands/core.py", line 1029, in invoke
    await injected(*ctx.args, **ctx.kwargs)  # type: ignore
  File "/home/runner/Capitol-Hill-Bot-1/.pythonlibs/lib/python3.10/site-packages/discord/ext/commands/core.py", line 244, in wrapped
    raise CommandInvokeError(exc) from exc
discord.ext.commands.errors.CommandInvokeError: Command raised an exception: RuntimeError: dictionary changed size during iteration

I've tried to get around this error by adding the users_to_remove portion, but to no avail. I need this to JUST remove those who are no longer in the server, not everyone.


Solution

  • Your error is in these lines:

    for user_id, user_data in bank.items():
        member = ctx.guild.get_member(user_id)
        if member is None:
            users_to_remove.append(user_id)
        for user_id in users_to_remove:
            del bank[user_id]
    

    You're removing items from bank while looping over the items view. What you can do instead is to use a list of keys as reference, separate from the dict:

    user_ids = list(bank.keys())
    
    for user_id in user_ids:
        user_data = bank.get(user_id)
        member = ctx.guild.get_member(user_id)
        if member is None:
            del bank[user_id]
    

    Your idea with users_to_remove could have worked, if you had changed its indentation to outside of the loop on bank.items():

    users_to_remove = []
    for user_id, user_data in bank.items():
        member = ctx.guild.get_member(user_id)
        if member is None:
            users_to_remove.append(user_id)
    
    for user_id in users_to_remove:
        del bank[user_id]