Tweepy isn't broken. Version 4.17.0 came out on PyPI in July 2026 and the library is still maintained. What changed is what sits behind it: Tweepy is a client for the official X API, so every read needs a developer account, an app and prepaid credits.
If your script only reads public posts and profiles, you can look for a Tweepy alternative that doesn't need those. If it posts as your users or needs X's own guarantees, keep Tweepy. This post shows how to tell which case you're in and what the swap looks like in code.
We run TwittrAPI, the API in the examples, so weigh that when you read the cost section. It's an unofficial API for reading public X data, and the trade-offs are real.
When to keep Tweepy
Tweepy is the right tool in these cases:
- You act for other users through OAuth. Sign in with X, posting on behalf of customers and similar flows belong on the official API.
- You need a contract with X. Some companies need the data to come from X directly, for compliance or procurement reasons.
- Your volume is tiny and you already have credits. A few hundred reads a month cost next to nothing either way, and migrating has a cost too.
When a Tweepy alternative makes sense
The usual reasons are cost and the access errors. A post read on the official API costs $0.005, so 100,000 posts a month is $500 (details and caps). Tweepy's search_recent_tweets also only covers the last seven days, per Tweepy's own docs, because that's what the underlying endpoint offers. If you keep hitting 403 or 453 errors, that's the access level and not your code.
If you came from snscrape rather than Tweepy, the snscrape and twscrape guide fits better.
What the Tweepy code looks like now
A typical search with Tweepy, using the official API:
import os
import tweepy
client = tweepy.Client(bearer_token=os.environ["X_BEARER_TOKEN"])
pages = tweepy.Paginator(
client.search_recent_tweets,
"python lang:en -is:retweet",
tweet_fields=["created_at", "public_metrics", "author_id"],
max_results=100,
limit=5,
)
for page in pages:
for tweet in page.data or []:
print(tweet.created_at, tweet.public_metrics["like_count"], tweet.text[:80])Note tweet_fields: on X API v2 you ask for each field by name, or you don't get it. Author names need expansions and a lookup in includes.
The same search without Tweepy
Create a key at /start (new accounts get $0.50 of test credit for 7 days), then:
import os
import time
import requests
API = "https://api.twittrapi.com"
HEADERS = {"X-API-KEY": os.environ["TWITTRAPI_API_KEY"]}
def paginate(path, max_pages=5, **params):
"""Stands in for tweepy.Paginator: yields one item at a time."""
params["format"] = "simple"
for _ in range(max_pages):
r = requests.get(f"{API}{path}", headers=HEADERS, params=params, timeout=30)
r.raise_for_status()
page = r.json()
yield from page["data"]
if not page["hasNextPage"]:
return
params["cursor"] = page["nextCursor"]
time.sleep(0.2)
results = paginate(
"/search",
searchItem="python lang:en -filter:nativeretweets",
category="Latest",
)
for tweet in results:
print(tweet["createdAt"], tweet["counts"]["likes"], tweet["author"]["username"], tweet["text"][:80])The differences worth noticing:
- All fields come back on every call, including the author. There's no
tweet_fieldsorexpansions. - The date range isn't limited to seven days. We ran
from:NASAsearches a month into the past for our operator guide without any issue. -is:retweetis also accepted, but-filter:nativeretweetsis the form in our search docs.category="Latest"gets you date order. The default,Top, is X's ranked selection.
Method mapping: Tweepy to TwittrAPI
Tweepy Client | Endpoint | Python SDK |
|---|---|---|
search_recent_tweets(q) | GET /search | client.search.tweets(q, category="Latest") |
search_all_tweets(q) | GET /search | same call, with since: and until: in the query |
get_users_tweets(id) | GET /user/{id}/tweets | client.users.tweets(id) |
get_user(username=...) | GET /user/by/username/{name} | client.users.get_by_username(name) |
get_users(ids=[...]) | GET /user/users/by/ids | client.users.get_many(ids) (500 per call) |
get_users_followers(id) | GET /user/{id}/followers | client.users.followers(id) |
get_users_following(id) | GET /user/{id}/followings | client.users.following(id) |
get_tweet(id) | GET /tweet/{id} | client.tweets.get(id) |
get_tweets(ids) | GET /tweet/tweets/by/ids | client.tweets.get_many(ids) (100 per call, SDK 1.3.0 or newer) |
get_retweeters(id) | GET /tweet/{id}/retweets | client.tweets.retweeters(id) |
get_quote_tweets(id) | GET /tweet/{id}/quotes | client.tweets.quotes(id) |
get_list_members(id) | GET /lists/members | client.lists.members(id) |
If you'd like a client library, the SDK replaces tweepy.Paginator with client.paginate:
from twittrapi import TwittrAPI
client = TwittrAPI(response_format="simple") # key from TWITTRAPI_API_KEY
for page in client.paginate(client.search.tweets, "python lang:en", category="Latest", max_pages=5):
for tweet in page["data"]:
print(tweet["text"][:80])Install it with pip install twittrapi.
Field mapping
This is where most of the migration work goes. Tweepy objects expose X's v2 names, and the simple format uses its own flat names.
| Tweepy (v2) | Simple format |
|---|---|
tweet.id (int) | tweet["id"] (string) |
tweet.created_at (datetime) | tweet["createdAt"] (ISO 8601 string) |
tweet.public_metrics["like_count"] | tweet["counts"]["likes"] |
tweet.public_metrics["retweet_count"] | tweet["counts"]["retweets"] |
tweet.public_metrics["reply_count"] | tweet["counts"]["replies"] |
tweet.public_metrics["quote_count"] | tweet["counts"]["quotes"] |
tweet.author_id | tweet["author"]["id"] |
tweet.conversation_id | tweet["conversationId"] |
tweet.in_reply_to_user_id | tweet["replyTo"]["userId"] (replyTo can be null) |
tweet.referenced_tweets | tweet["replyTo"], tweet["quoted"], tweet["retweetOf"] |
user.username | user["username"] |
user.public_metrics["followers_count"] | user["counts"]["followers"] |
meta["next_token"] | page["nextCursor"] |
Two traps. IDs are strings here, so any database column or comparison that expects an int needs changing. And on a repost, the wrapper's own like count was 0 in our tests while the real number sat in retweetOf, so read tweet["retweetOf"]["counts"] for those or filter reposts out.
Page size works differently too. max_results on X API v2 is a number between 10 and 100, while count here is only a hint to X. Asking for 5 items from a media timeline gave us 2 in one test. Don't write code that assumes a page is full.
What it costs
| Workload | Official X API (via Tweepy) | TwittrAPI Standard |
|---|---|---|
| 100,000 posts a month by search | $500 | about $0.50 (5,000 calls) |
| 10,000 profile lookups, batched | $100 | $0.002 (20 calls of 500) |
| 1,000 single post lookups | $5 | $0.10 |
These use each side's published rates: $0.005 per post and $0.010 per user on the official API, and $0.10 per 1,000 calls for us, assuming about 20 posts per search page. X decides the real page size, so measure yours. The price comparison includes other providers, some of which may suit you better.
What you give up
An unofficial API reads X the way X's own apps do. X can change things without notice and we have to adapt when it does, which isn't the same as having a contract with X. Likes are private on X, so "who liked this post" only works for your own posts, with your own session. Posting works through account actions, which need your session cookie and a proxy, and that is a different setup from OAuth.
Test with your real queries before you change anything in production: $0.50 of test credit is about 5,000 calls.
Frequently asked questions
Is Tweepy free to use?
The library is free and open source. The API it calls is not: the official X API needs a developer account and prepaid credits for reads, so Tweepy costs whatever your calls cost.
Does Tweepy still work in 2026?
Yes. Version 4.17.0 was released in July 2026. Most errors people run into are access errors from the X API, such as 403 and 453, and not bugs in Tweepy.
Can I use Tweepy without an API key?
No. Tweepy needs credentials from the X developer console. To read public X data without a developer account, you need a third-party API, which has its own key.
What's the difference between tweepy.API and tweepy.Client?
tweepy.API calls the older v1.1 endpoints and tweepy.Client calls v2. Code using api.search_tweets is on v1.1, which is where the 453 error usually appears.
How long does it take to migrate from Tweepy?
A short read script takes under an hour: swap the calls using the table above and rename the fields. Budget more if IDs are stored as integers or if you post on behalf of users, as those parts should stay on the official API.
Sources
- Tweepy on PyPI: version 4.17.0, uploaded 2 July 2026
- Tweepy Client reference: seven-day recent search, authentication,
max_results - TwittrAPI simple format and search reference
- Pricing: rates as of November 2026
TwittrAPI is not affiliated with X Corp. It reads only public data.



