Tutorial: Creating a Twitter (X) Bot using Python

Published at Oct 6, 2024

5 min read
#python
#twitter
#api
#bot
#tutorial

At my last semester at university, I was really struggling with my courses. I was overwhelmed. I thought that “Time just doesn’t pass by”, then I came up with a simple project to remind me that time was passing. That’s how I started to build an X bot that tweets the percentage of the semester that has passed. In this article, I will show you how to build a X bot using Python and the X API.

This tutorial is up-to-date as of October 2024 using the new X API. If you are reading this in the future, please check the official documentation for any changes.

Here is an example tweet from the bot:

I know most of my audience doesn’t speak Turkish, but the tweet simply says that the semester has progressed by 6.61% and there are 88 days left until the end of the semester.

You can reach the source code of the project from here.

Structure

Features

  • Post a tweet autonomously.
  • Schedule the bot to tweet at a specific time, you can also set the interval.
  • Retry posting the tweet if it fails.
  • Create a beautiful progress graph that shows the progress of the semester.
  • Completely free to deploy and use.

Getting Started

First, you need to create an X account for your bot. Then, you need to sign-in to the X Developer Portal and create a new project. After creating a project, you need to create an app and get the API keys.

X Developer Portal
X Developer Portal

Now, copy all of these keys and paste them into a .env file in the root directory of your project. Here is an example of the .env file:

API_KEY=YOUR_API_KEY
API_KEY_SECRET=YOUR_API_KEY_SECRET
BEARER_TOKEN=YOUR_BEARER
ACCESS_TOKEN=YOUR_ACCESS_TOKEN
ACCESS_TOKEN_SECRET=YOUR_ACCESS_TOKEN_SECRET
CLIENT_ID=YOUR_CLIENT_ID
CLIENT_SECRET=YOUR_CLIENT_SECRET

Hint: Notice that we ignore the .env file in the .gitignore file so that we don’t share our keys with the public.

Requirements

First, you can find the required libraries in the requirements.txt file. You can install them by running:

pip install -r requirements.txt

I will explain why we need each required library:

  • tweepy: Tweepy is an easy-to-use Python library for accessing the X API.
  • python-dotenv: Reads the key-value pair from .env file and adds them to the environment variable.
  • matplotlib and numpy: We use these libraries to create the graph that shows the progress of the semester.
  • schedule: We use this library to schedule the bot to tweet at a interval or at a specific time.

Writing the Bot

  • We set up basic logging to see the logs of the bot:
# Setup basic logging
logging.basicConfig(
    level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
  • Then, I have a very simple code that calculates how many seconds have passed since the beginning of the semester and how many seconds are left until the end of the semester. Then I calculate the percentage of the semester that has passed. Pretty simple, right?

  • After calculating the percentage, I create a graph that shows the progress of the semester using the matplotlib library. I’m not going to show the code snippet here, but you can find it in the source code.

  • I have two functions that connect to the X API and post the progress image on X:

def connect_twitter() -> tuple:
    """
    Connect to the X API using Tweepy and environment variables.

    Returns:
        tuple: A tuple containing the X client and API instances.
    """
    load_dotenv()
    tweepy_auth = tweepy.OAuth1UserHandler(
        os.getenv("API_KEY"),
        os.getenv("API_KEY_SECRET"),
        os.getenv("ACCESS_TOKEN"),
        os.getenv("ACCESS_TOKEN_SECRET"),
    )
    api = tweepy.API(tweepy_auth)
    client = tweepy.Client(
        os.getenv("BEARER_TOKEN"),
        os.getenv("API_KEY"),
        os.getenv("API_KEY_SECRET"),
        os.getenv("ACCESS_TOKEN"),
        os.getenv("ACCESS_TOKEN_SECRET"),
    )
    logging.info("Connected to Twitter API")
    return client, api

def post_photo():
    """
    Generate and post a progress image on X with the remaining days and progress.
    """
    client, api = connect_twitter()
    remaining_days = (END_DATE - datetime.now()).days # END_DATE is the end date of the semester
    percentage = calculate_percentage(START_DATE, END_DATE) # START_DATE is the start date of the semester
    img_path = create_progress_image(percentage)

    text = f"🔴 ODTÜ'de 2024-2025 güz dönemi ilerlemesi: %{percentage} \n🗓️ Kalan gün sayısı: {remaining_days}"
    media = api.media_upload(filename=img_path)
    client.create_tweet(text=text, media_ids=[media.media_id])
    logging.info("Successfully posted progress image on Twitter")
  • Error handling is important when working with external APIs. Sometimes the bot may fail to post the tweet because of a network issue or some other reason. In this case, we need to retry posting after a delay, allowing multiple attempts in case of failure.
def retry(max_retries: int = MAX_RETRIES, retry_delay: int = RETRY_DELAY):
    """
    Decorator to retry a task on failure.

    Args:
        max_retries (int): Maximum number of retries (default: 3).
        retry_delay (int): Delay between retries in seconds (default: 300).
    """

    def decorator(func):
        def wrapper(*args, **kwargs):
            attempts = 0
            while attempts < max_retries:
                try:
                    result = func(*args, **kwargs)
                    logging.info("Task completed successfully.")
                    return result
                except Exception as e:
                    attempts += 1
                    logging.error(
                        f"Task failed: {e}. Retrying {attempts}/{max_retries} in {retry_delay} seconds..."
                    )
                    time.sleep(retry_delay)
            logging.error("Max retries reached. Task failed permanently.")

        return wrapper

    return decorator
  • We wrap the post_photo function with the retry decorator:
@retry()
def scheduled_post_photo():
    """A wrapper around the post_photo function to be used for scheduling with retries."""
    post_photo()
  • Then, I have a function that schedules the bot to post the progress photo at specified times:
def schedule_tasks():
    """
    Schedule tasks to post the progress photo at specified times.
    """
    schedule.every().day.at("10:40").do(scheduled_post_photo)
    schedule.every().day.at("17:30").do(scheduled_post_photo)
    logging.info("Scheduled tasks successfully")
  • Finally, I have a main function that runs the bot. If you keep the POLLING_INTERVAL large enough (60 seconds is enough in my case), the bot will consume very little CPU time.

Hint: You could also use a tool like cron to schedule the bot to run at a specific time and remove the while loop. However this was a 1 day side project and I found it easier to do everything in Python.

if __name__ == "__main__":
    schedule_tasks()
    while True:
        schedule.run_pending()
        time.sleep(POLLING_INTERVAL)

Deployment

I deployed the function on a free-tier of PythonAnywhere. Upload your environment file and the source code to the server, install the required libraries, and run the bot 7/24. It is that simple and only takes a few minutes!

Conclusion

This project is just the beginning. You can extend the bot to post about other events or even integrate it with other APIs. I encourage you to explore different ways to make this bot more interactive. Have fun experimenting, and feel free to reach out with any questions or feedback!

← Back to Blog