Beginner
Twitter Bots can be super useful to help automate some of the interactions on social media in order to build and grow engagement but also automate some tasks. There has been many changes on the twitter developer account and sometimes it’s uncertain how to even create a tweet bot. This article will walk through step bey step on how to create a twitter bot with the latest Twitter API v2 and also provide some code you can copy and paste in your next project. We also end with how to create a more useful bot that can post some articles about python automatically.
In a nutshell, how a twitter bot works is that you will need to run your code for a twitter bot in your own compute that can be triggered from a Twitter webhook (not covered) which is called by twitter based on a given event, or by having your program run periodically to read and send tweets (covered in this article). Either way, there are some commonalities and in this article we will walk through how to read tweets, and then to send tweets which are from google news related to python!
Step 1: Sign up for Developer program
If you haven’t already you will need to either sign in or sign up for a twitter account through twitter.com. Make sure your twitter account has an email address allocated to it (if you’re not aware, you can create a twitter account with just your mobile phone number)

Next go to developer.twitter.com and sign up for the developer program (yes, you need to sign up for a second time). This enables you to create applications.

First you’ll need to answer some questions on purpose of the developer account. You can chose “Make a Bot”

Next you will need to agree to the terms and conditions, and then a verification email will be sent to your email address from your twitter account.
When you click on the email to verify your account, you can then enter your app name. This is an internal name and something that will make it easy for you to reference.

Once you click on keys, you will then be given a set of security token keys like below. Please copy them in a safe place as your python code will need to use them to access your specific bot. If you do lose your keys, or someone gets access to them for some reason, you can generate new keys from your developer.twitter.com console.
There are two keys which you will need to use:
- API Key (think of this like a username)
- API Key Secret (think of this like a password)
- Bearer Token (used for read queries such as getting latest tweets)
There is also a third key, a Bearer Token, but this you can ignore. It is for certain types of requests

At the bottom of the screen you’ll see a “Skip to Dashboard”, when you click on that you’ll then see the overview of your API metrics.
Within this screen you can see the limits of the number of calls per month for example and how much you have already consumed.

Next, click on the project and we have to generate the access tokens. Currently with the previous keys you can only read tweets, you cannot create ones as yet.
After clicking on the project, chose the “keys and tokens” tab and at the bottom you can generate the “Access Tokens”. In this screen you can also re-generate the API Keys and Bearer Token you just created before in case your keys were compromised or you forgot them.

Just like before, generate the keys and copy them.

By now, you have 5 security toknes:
- API Key – also known as the Consumer Key (think of this like a username)
- API Key Secret – also known as the Consumer Secret (think of this like a password)
- Bearer Token (used for read queries such as getting latest tweets)
- Access Token (‘username’ to allow you to create tweets)
- Access Token Secret (‘password’ to allow you to create tweets)
Step 2: Test your twitter API query
Now that you have the API keys, you can do some tests. If you are using a linux based machine you can use the curl command to do a query. Otherwise, you can use a site such as https://reqbin.com/curl to do an online curl request.
Here’s a simple example to get the most recent tweets. It uses the API https://api.twitter.com/2/tweets/search/recent which must include the query keyword which includes a range of parameter options (find out the list in the twitter query documentation).
curl --request GET 'https://api.twitter.com/2/tweets/search/recent?query=from:pythonhowtocode' --header 'Authorization: Bearer <your bearer token from step 1>'
The output is as follows:
{
"data": [{
"id": "1523251860110405633",
"text": "See our latest article on THE complete beginner guide on creating a #discord #bot in #python \n\nEasily add this to your #100DaysOfCode #100daysofcodechallenge #100daysofpython \n\nhttps://t.co/4WKvDVh1g9"
}],
"meta": {
"newest_id": "1523251860110405633",
"oldest_id": "1523251860110405633",
"result_count": 1
}
}
Here’s a much more complex example. This includes the following parameters:
%23– which is the escape characters for#and searches for hashtags. Below example is hashtag#python(case insensitive)%20– this is an escape character for a space and separates different filters with anANDoperation-is:retweet– this excludes retweets. The ‘-‘ sign preceding theisnegates the actual filter-is:reply– this excludes replies. The ‘-‘ sign preceding theisnegates the actual filtermax_results=20– an integer that defines the maximum number of return results and in this case 20 resultsexpansions=author_id– this makes sure to include the username internal twitter id and also the actual username under anincludessection at the bottom of the returned JSONtweet.fields=public_metrics,created_at– returns the interaction metrics such as number of likes, number of retweets, etc as well as the time (in GMT timezone) when the tweet was createduser.fields=created_at,location– this returns when the user account was created and the user self-reported location in their profile.
curl --request GET 'https://api.twitter.com/2/tweets/search/recent?query=%23python%20-is:retweet%20-is:reply&max_results=20&expansions=author_id&tweet.fields=public_metrics,created_at&user.fields=created_at,location' --header 'Authorization: Bearer <Your Bearer Token from Step 1>'
Result of this looks like the following – notice that the username details is in the includes section below where you can link the tweet with the username with the author_id field.
{{
"data": [{
"id": "1523688996676812800",
"text": "NEED a #JOB?\nSign up now https://t.co/o7lVlsl75X\nFREE. NO MIDDLEMEN\n#Jobs #AI #DataAnalytics #MachineLearning #Python #JavaScript #WomenWhoCode #Programming #Coding #100DaysofCode #DEVCommunity #gamedev #gamedevelopment #indiedev #IndieGameDev #Mobile #gamers #RHOP #BTC #ETH #SOL https://t.co/kMYD2417jR",
"author_id": "1332714745871421443",
"public_metrics": {
"retweet_count": 3,
"reply_count": 0,
"like_count": 0,
"quote_count": 0
},
"created_at": "2022-05-09T15:39:00.000Z"
},
....
}],
"includes": {
"users": [{
"name": "Job Preference",
"id": "1332714745871421443",
"username": "JobPreference",
"created_at": "2020-11-28T15:56:01.000Z"
},
....
}
Step 3: Reading tweets with python code
Building on top of the tests conducted on Step 2, it is a simple extra step in order to convert this to python code using the requests module which we’ll show first and after show a simpler way with the library tweepy. You can simply use the library to convert the curl command into a bit of python code. Here’s a structured version of this code where the logic is encapsulated in a class.
import requests, json
from urllib.parse import quote
from pprint import pprint
class TwitterBot():
URL_SEARCH_RECENT = 'https://api.twitter.com/2/tweets/search/recent'
def __init__(self, bearer_key):
self.bearer_key = bearer_key
def search_recent(self, query, include_retweets=False, include_replies=False):
url = self.URL_SEARCH_RECENT + "?query=" + quote(query)
if not include_retweets: url += quote(' ')+'-is:retweet'
if not include_replies: url += quote(' ')+'-is:reply'
url += '&max_results=20&expansions=author_id&tweet.fields=public_metrics,created_at&user.fields=created_at,location'
headers = {'Authorization': 'Bearer ' + self.bearer_key }
r = requests.get(url, headers = headers)
r.encoding = r.apparent_encoding. #Ensure to use UTF-8 if unicode characters
return json.loads(r.text)
#create an instance and pass in your Bearer Token
t = TwitterBot('<Insert your Bearer Token from Step 1>')
pprint( t.search_recent( '#python') )
The above code is fairly straightforward and does the following:
TwitterBot class– this class encapsulates the logic to send the API requestsTwitterBot.search_recent– this method takes in the query string, then escapes any special characters, then calls therequests.get()to call thehttps://api.twitter.com/2/tweets/search/recentAPI callpprint()– this simply prints the output in a more readable format
This is the output:


However, there is a simpler way which is to use tweepy.
pip install tweepy
Next you can use the tweepy module to search recent tweets:
import tweepy
client = tweepy.Client(bearer_token='<insert your token here from previous step>')
query = '#python -is:retweet -is:reply' #exclude retweets and replies with '-'
tweets = client.search_recent_tweets( query=query,
tweet_fields=['public_metrics', 'context_annotations', 'created_at'],
user_fields=['username','created_at','location'],
expansions=['entities.mentions.username','author_id'],
max_results=10)
#The details of the users is in the 'includes' list
user_data = {}
for raw_user in tweets.includes['users']:
user_data[ raw_user.id ] = raw_user
for index, tweet in enumerate(tweets.data):
print(f"[{index}]::@{user_data[tweet.author_id]['username']}::{tweet.created_at}::{tweet.text.strip()}\n")
print("------------------------------------------------------------------------------")
Output as follows:

Please note, that after calling the API a few times your number of tweets consumed will have increased and may have hit the limit. You can always visit the dashboard at https://developer.twitter.com/en/portal/dashboard to see how many requests have been consumed. Notice, that this does not count the number of actual API calls but the actual number of tweets. So it can get consumed pretty quickly.

Step 4: Sending out a tweet
So far we’ve only been reading tweets. In order to send a tweet you can use the create_tweet() function of tweepy.
client = tweepy.Client( consumer_key= "<API key from above - see step 1>",
consumer_secret= "<API Key secret - see step 1>",
access_token= "<Access Token - see step 1>",
access_token_secret= "<Access Token Secret - see step 1>")
# Replace the text with whatever you want to Tweet about
response = client.create_tweet(text='A little girl walks into a pet shop and asks for a bunny. The worker says” the fluffy white one or the fluffy brown one”? The girl then says, I don’t think my python really cares.')
print(response)
Output from Console:

Output from Twitter:

How to Send Automated Tweets About the Latest News
To make this a bit more of a useful bot rather than simply tweet out static text, we’ll make it tweet about the latest things happened in the news about python.
In order to search for news information, you can use the python library pygooglenews
pip install pygooglenews
The library searches Google news RSS feed and was developed by Artem Bugara. You can see the full article of he developed the Google News library. You can put in a keyword and also time horizon to make it work. Here’s an example to find the latest python articles in last 24 hours.
from pygooglenews import GoogleNews
gn = GoogleNews()
search = gn.search('python programming', when = '12h')
for article in search['entries']:
print(article.title)
print(article.published)
print(article.source.title)
print('-'*80) #string multiplier - show '-' 80 times
Here’s the output:
So, the idea would be to show a random article on the twitter bot which is related to python programming. The gn.search() functions returns a list of all the articles under the entries dictionary item which has a list of those articles. We will simply pick a random one and construct the tweet with the article title and the link to the article.
import tweepy
from pygooglenews import GoogleNews
from random import randint
client = tweepy.Client( consumer_key= "<your consumer/API key - see step 1>",
consumer_secret= "<your consumer/API secret - see step 1>",
access_token= "<your access token key - see step 1>",
access_token_secret= "<your access token secret - see step 1>")
gn = GoogleNews()
search = gn.search('python programming', when = '24h')
#Find random article in last 24 hours using randint between index 0 and the last index
article = search['entries'][ randint( 0, len( search['entries'])-1 ) ]
#construct the tweet text
tweet_text = f"In python news: {article.title}. See full article: {article.link}. #python #pythonprogramming"
#Fire off the tweet!
response = client.create_tweet( tweet_text )
print(response)
Output from the console on the return result:

And, most importantly, here’s the tweet from our @pythonhowtocode! Twitter automatically pulled the article image

This has currently been scheduled as a daily background job!
How To Use Python Decorators: A Complete Guide
Intermediate
You’ve probably seen the @ symbol above function definitions in Python code and wondered what it does. That’s a decorator — one of Python’s most powerful and elegant features. Decorators let you wrap a function with additional behavior (logging, caching, access control, rate limiting, timing) without modifying the function’s code. They’re the reason you can add authentication to a Flask route with a single line, or enable caching with @functools.lru_cache.
Decorators are a pure Python feature — no installation required. They’re built on Python’s first-class functions (functions that can be passed as arguments and returned from other functions). Once you understand how decorators work mechanically, you’ll be able to read and write the patterns used by virtually every Python framework, from Django’s @login_required to FastAPI’s @app.get() to pytest’s @pytest.fixture.
In this tutorial, you’ll learn how decorators work from first principles, how to use functools.wraps to preserve function metadata, how to write parameterized decorators (decorators that take arguments), how to stack multiple decorators, how to use class-based decorators, and how to apply these techniques in real-world scenarios like timing, retry logic, and access control.
Decorators: Quick Example
Here’s the simplest useful decorator — one that logs when a function is called:
# decorator_quick.py
import functools
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}({args}, {kwargs})")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper
@log_calls
def add(a, b):
return a + b
# This is equivalent to: add = log_calls(add)
result = add(3, 4)
print(f"Final result: {result}")
# The function's identity is preserved
print(f"Function name: {add.__name__}")
Output:
Calling add((3, 4), {})
add returned 7
Final result: 7
Function name: add
The @log_calls syntax is shorthand for add = log_calls(add). The decorator receives the original function, returns a new wrapper function that adds behavior before and after calling the original, and replaces the name add with the wrapper. The @functools.wraps(func) line copies the original function’s name, docstring, and other metadata onto the wrapper — always include this.
How Decorators Work: First Principles
To truly understand decorators, you need to understand that in Python, functions are objects — they can be passed as arguments and returned from other functions. This is called “first-class functions.” Decorators are just a syntax shortcut for a function transformation pattern.
# first_class_functions.py
# Functions can be passed as arguments
def apply_twice(func, value):
return func(func(value))
def double(x):
return x * 2
result = apply_twice(double, 3)
print(f"Apply twice: {result}") # 3 -> 6 -> 12
# Functions can be returned from other functions
def make_multiplier(n):
def multiplier(x):
return x * n
return multiplier # Returns the inner function
triple = make_multiplier(3)
print(f"Triple 5: {triple(5)}") # 15
# The decorator pattern manually, without @ syntax
def shout(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper() + "!!!"
return wrapper
def greet(name):
return f"Hello, {name}"
# Without @ syntax -- same result
greet = shout(greet)
print(greet("alice")) # HELLO, ALICE!!!
Output:
Apply twice: 12
Triple 5: 15
HELLO, ALICE!!!
The key insight: @shout above a function definition is exactly equivalent to writing greet = shout(greet) after the definition. The @ syntax just makes it more readable and places the decoration visually near the function definition where it belongs.
Always Use functools.wraps
Without @functools.wraps(func), your decorator replaces the original function’s metadata with the wrapper’s. This causes problems with debugging, documentation, and tools that inspect function names. Always include it:
# wraps_example.py
import functools
# WITHOUT functools.wraps -- breaks function identity
def bad_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
# WITH functools.wraps -- preserves identity
def good_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@bad_decorator
def my_function_bad():
"""This function does something important."""
pass
@good_decorator
def my_function_good():
"""This function does something important."""
pass
print(f"Bad decorator name: {my_function_bad.__name__}")
print(f"Bad decorator docstr: {my_function_bad.__doc__}")
print()
print(f"Good decorator name: {my_function_good.__name__}")
print(f"Good decorator docstr: {my_function_good.__doc__}")
Output:
Bad decorator name: wrapper
Bad decorator docstr: None
Good decorator name: my_function_good
Good decorator docstr: This function does something important.
Practical Decorator Examples
Timing Functions
A timer decorator measures how long a function takes to execute — great for performance monitoring and identifying bottlenecks:
# timer_decorator.py
import functools
import time
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f} seconds")
return result
return wrapper
@timer
def slow_function():
time.sleep(0.1)
return "done"
@timer
def sum_million():
return sum(range(1_000_000))
slow_function()
result = sum_million()
print(f"Sum result: {result:,}")
Output:
slow_function took 0.1002 seconds
sum_million took 0.0312 seconds
Sum result: 499,999,500,000
Retry Logic
A retry decorator automatically re-runs a function if it raises an exception — essential for network calls, database operations, and any code that can fail transiently:
# retry_decorator.py
import functools
import time
import random
def retry(max_attempts=3, delay=1.0, exceptions=(Exception,)):
"""Decorator factory: retries a function up to max_attempts times."""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
last_error = None
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
last_error = e
print(f"Attempt {attempt}/{max_attempts} failed: {e}")
if attempt < max_attempts:
time.sleep(delay)
raise last_error
return wrapper
return decorator
# Simulated unreliable function (fails 70% of the time)
call_count = 0
@retry(max_attempts=5, delay=0.1, exceptions=(ValueError,))
def unreliable_api_call():
global call_count
call_count += 1
if random.random() < 0.7:
raise ValueError(f"API timeout on call #{call_count}")
return f"Success on call #{call_count}"
random.seed(42)
result = unreliable_api_call()
print(f"Final result: {result}")
Output:
Attempt 1/5 failed: API timeout on call #1
Attempt 2/5 failed: API timeout on call #2
Attempt 3/5 failed: API timeout on call #3
Final result: Success on call #4
Notice the decorator factory pattern: retry(max_attempts=5, delay=0.1) returns a decorator, which then returns a wrapper. This is a three-level nesting -- outer function configures, middle function receives the function to decorate, inner function is what actually runs. This is the standard pattern for parameterized decorators.
Parameterized Decorators
When your decorator needs configuration (like the number of retries in the example above), you add one more level of nesting -- a "decorator factory" that takes the parameters and returns the actual decorator:
# parameterized_decorator.py
import functools
def repeat(n):
"""Call the decorated function n times."""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
results = []
for _ in range(n):
results.append(func(*args, **kwargs))
return results
return wrapper
return decorator
@repeat(3)
def say_hello(name):
return f"Hello, {name}!"
results = say_hello("Alice")
for r in results:
print(r)
Output:
Hello, Alice!
Hello, Alice!
Hello, Alice!
Stacking Multiple Decorators
You can apply multiple decorators to the same function by stacking them. They apply from bottom to top (closest to the function first):
# stacking_decorators.py
import functools
import time
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
print(f" [timer] {func.__name__}: {time.perf_counter()-start:.4f}s")
return result
return wrapper
def log_result(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
print(f" [log] {func.__name__} returned: {result}")
return result
return wrapper
# Applied bottom-up: log_result wraps the original,
# then timer wraps log_result's wrapper
@timer
@log_result
def compute(x, y):
return x ** y
result = compute(2, 10)
print(f"Final result: {result}")
Output:
[log] compute returned: 1024
[timer] compute: 0.0001s
Final result: 1024
Real-Life Example: Access Control Decorators
Here's a practical access control system using decorators -- the same pattern used by web frameworks for route authentication:
# access_control.py
import functools
# Simulated current user session
current_user = {'name': 'alice', 'roles': ['user', 'editor'], 'logged_in': True}
def login_required(func):
"""Decorator that requires the user to be logged in."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
if not current_user.get('logged_in'):
print(f"Access denied: login required for {func.__name__}")
return None
return func(*args, **kwargs)
return wrapper
def require_role(role):
"""Decorator factory: requires the user to have a specific role."""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if role not in current_user.get('roles', []):
print(f"Access denied: '{role}' role required for {func.__name__}")
return None
return func(*args, **kwargs)
return wrapper
return decorator
@login_required
def view_dashboard():
return f"Dashboard for {current_user['name']}"
@login_required
@require_role('admin')
def delete_user(user_id):
return f"Deleted user {user_id}"
@login_required
@require_role('editor')
def publish_post(post_id):
return f"Published post {post_id}"
# Alice is logged in and has 'editor' but not 'admin'
print(view_dashboard())
print(delete_user(42))
print(publish_post(101))
# Simulate a logged-out user
current_user['logged_in'] = False
print(view_dashboard())
Output:
Dashboard for alice
Access denied: 'admin' role required for delete_user
Published post 101
Access denied: login required for view_dashboard
This is the exact pattern used by Flask's @login_required and Django's @permission_required. The decorators are reusable across any number of functions -- add access control to a new function by adding one line above its definition. The stacked @login_required @require_role('admin') means the user must pass both checks: logged in AND has the required role.
Frequently Asked Questions
When should I use a decorator instead of a helper function?
Use a decorator when you want to add the same cross-cutting behavior (logging, timing, validation, caching) to multiple functions without repeating the logic. If you find yourself writing the same "before" and "after" code in many functions, that's a strong signal to extract it into a decorator. For one-off or highly specific behavior, a regular helper function is simpler.
Can I use a class as a decorator?
Yes -- any callable can be a decorator. A class with a __call__ method works as a decorator. Class-based decorators are useful when you need to maintain state between calls (like call counts or cached results). Define __init__(self, func) to receive the function and __call__(self, *args, **kwargs) to wrap it. The @functools.wraps(func) approach works on __call__ too.
Do decorators work on class methods?
Yes, but with one caveat: the first argument of instance methods is self. Since decorators use *args, **kwargs, this is handled automatically. However, @staticmethod and @classmethod are themselves decorators. When stacking with them, always place @staticmethod or @classmethod outermost (closest to the def).
What is @functools.lru_cache and when should I use it?
@functools.lru_cache(maxsize=128) memoizes a function's return values -- if the function is called again with the same arguments, it returns the cached result instead of recomputing. Use it for pure functions (no side effects) that are called repeatedly with the same inputs. It's especially powerful for recursive functions like Fibonacci where the same sub-problems repeat many times.
Why does my IDE show wrong type hints after applying a decorator?
Without @functools.wraps, the decorated function's signature shows as (*args, **kwargs) -- losing the original type hints. With @functools.wraps, the function identity is preserved, but the signature the type checker sees is still the wrapper's. For full type hint preservation in decorated functions, use typing.ParamSpec and typing.Concatenate (Python 3.10+) to annotate the wrapper correctly.
Conclusion
Decorators are one of Python's most powerful code-reuse mechanisms. In this tutorial, you learned how Python's first-class functions make decorators possible, why @functools.wraps(func) is essential in every decorator, how to write practical decorators for timing, retry logic, and logging, how to create parameterized decorators using a decorator factory pattern, how to stack multiple decorators on a single function, and how the access control pattern mirrors real framework implementations.
The access control project is a foundation you can extend: add role inheritance, time-based access restrictions, or rate limiting. Every web framework you'll encounter -- Flask, Django, FastAPI -- relies heavily on decorators for its most important features.
For deeper coverage, see the functools module documentation and PEP 318 which introduced decorator syntax to Python.
Related Articles
Further Reading: For more details, see the Python HTTP client documentation.
Pro Tips for Building a Better Twitter Bot
1. Respect Rate Limits with Exponential Backoff
The Twitter API enforces strict rate limits. Instead of crashing when you hit one, implement exponential backoff to retry gracefully. Wrap your API calls in a retry function that doubles the wait time after each failed attempt, starting from 1 second up to a maximum of 64 seconds. This keeps your bot running reliably without getting your credentials revoked.
# rate_limit_handler.py
import time
import requests
def api_call_with_backoff(url, headers, max_retries=5):
wait_time = 1
for attempt in range(max_retries):
response = requests.get(url, headers=headers)
if response.status_code == 200:
return response.json()
elif response.status_code == 429:
print(f"Rate limited. Waiting {wait_time}s...")
time.sleep(wait_time)
wait_time = min(wait_time * 2, 64)
else:
response.raise_for_status()
raise Exception("Max retries exceeded")
Output:
Rate limited. Waiting 1s...
Rate limited. Waiting 2s...
{'data': [{'id': '1234567890', 'text': 'Hello world'}]}
2. Never Hardcode API Keys
Store your API credentials in environment variables or a .env file, never in your source code. If you accidentally push hardcoded keys to a public GitHub repo, bots will find and abuse them within minutes. Use the python-dotenv library to load credentials from a .env file that you add to your .gitignore.
# secure_credentials.py
import os
from dotenv import load_dotenv
load_dotenv()
BEARER_TOKEN = os.getenv("TWITTER_BEARER_TOKEN")
API_KEY = os.getenv("TWITTER_API_KEY")
API_SECRET = os.getenv("TWITTER_API_SECRET")
if not BEARER_TOKEN:
raise ValueError("TWITTER_BEARER_TOKEN not set in .env file")
3. Add Logging Instead of Print Statements
Replace print() calls with Python’s built-in logging module. Logging gives you timestamps, severity levels, and the ability to write to files — essential for debugging a bot that runs unattended. When your bot tweets something unexpected at 3 AM, logs are the only way to figure out what happened.
# bot_with_logging.py
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler("bot.log"),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
logger.info("Bot started successfully")
logger.warning("Approaching rate limit: 14/15 requests used")
logger.error("Failed to post tweet: 403 Forbidden")
Output:
2026-03-26 10:15:30 [INFO] Bot started successfully
2026-03-26 10:15:31 [WARNING] Approaching rate limit: 14/15 requests used
2026-03-26 10:15:32 [ERROR] Failed to post tweet: 403 Forbidden
4. Track Posted Content to Avoid Duplicates
Bots that post the same content repeatedly get flagged and suspended. Keep a simple record of what you have already tweeted using a JSON file or SQLite database. Before posting, check if the content has been posted before. This is especially important for news bots that might encounter the same story from multiple sources.
5. Use a Scheduler for Consistent Posting
Instead of running your bot in a loop with time.sleep(), use a proper scheduler like schedule or APScheduler. Schedulers handle timing more reliably, support cron-like expressions, and make it easy to run different tasks at different intervals. For production bots, consider using system-level scheduling with cron (Linux) or Task Scheduler (Windows).
Frequently Asked Questions
Can I still build a Twitter bot with the API?
Yes, but access has changed. The free tier of the X (formerly Twitter) API v2 allows basic posting. For reading tweets or higher volume, you need a paid plan. Check current pricing at developer.x.com.
What Python library should I use for the Twitter/X API?
Use tweepy for the most mature Python wrapper with v2 API support. It handles OAuth 2.0 authentication, rate limiting, and provides clean methods for posting, searching, and streaming.
How do I authenticate with the Twitter API v2?
Use OAuth 2.0 Bearer Token for read-only access or OAuth 1.0a for posting. Generate credentials in the X Developer Portal, then pass them to tweepy.Client().
What are the rate limits for the Twitter API?
Rate limits vary by endpoint and plan. The free tier allows 1,500 tweets per month. Always implement rate limit handling with tweepy’s wait_on_rate_limit=True.
What can a Twitter bot do?
Bots can auto-post content, reply to mentions, retweet by keyword, track hashtags, analyze sentiment, and provide automated responses. Always follow the X API terms of service.
Hey,
Thank you so much! I have tried sample codes from other tutorials, including twitter API documentation and none of that really worked. Your code works nice, thank you really.
David
Thanks for the feedback, glad it was helpful.