PYTHON
Twitter API Python client
How do I call the Twitter API from Python?
The Python client for TwitterAPIs is a requests session with one header on it. There is no package to install beyond requests itself, no OAuth flow, and no token to refresh, because the key is a static Bearer credential that goes on every call. Set it once on a session, write one helper that builds the URL and decodes the JSON, and you have covered every read endpoint the API serves.
pip install requests
import os
import requests
BASE = "https://api.twitterapis.com"
KEY = os.environ["TWITTERAPIS_KEY"]
session = requests.Session()
session.headers["Authorization"] = f"Bearer {KEY}"
def get(path, **params):
"""Call any read endpoint. path is the docs path, e.g. 'user/info'."""
res = session.get(f"{BASE}/twitter/{path}", params=params, timeout=30)
res.raise_for_status()
return res.json()
# Resolve a handle to the permanent numeric user ID
profile = get("user/info", username="naval")
print(profile["user"]["id"], profile["user"]["followers_count"])
# Search posts, then page with the returned cursor
page = get("tweet/advanced_search", query="from:naval min_faves:500")
for tweet in page["tweets"]:
print(tweet["id"], tweet["text"][:80])Paginating a follower list
Cursor-paginated endpoints return a next cursor alongside the page of results. Wrapping that in a generator keeps the cursor logic in one place, so the calling code is an ordinary loop that never has to know pagination exists.
How many times that loop runs is the number worth knowing before you start it, because billing is per call rather than per record. Across 396,817 successful tweet-returning read calls on our own billing logs, 13 to 17 August 2026, a timeline read returned 18.78 records on average and a search read returned 7.62, with 29.5% of search calls returning nothing at all and still counting. Divide the records you need by the figure for the endpoint you are actually calling, not by the page size you asked for.
def paginate(path, collection, max_calls=500, **params):
"""Yield each page's ROWS. Stops on an empty page, on a spent cursor, or at
max_calls, whichever comes first.
Both terminators are required. Search and list endpoints null the cursor on a
page that is still FULL, so an empty-page check alone drops the last page.
Follower-graph endpoints never null the cursor, so a cursor check alone never
returns. params is never mutated: a stale cursor left in it re-fetches the
same page forever."""
cursor, calls, seen = None, 0, set()
while calls < max_calls:
page = get(path, **({**params, "cursor": cursor} if cursor else params))
calls += 1
rows = page.get(collection) or []
if not rows:
return
yield rows
cursor = page.get("next_cursor")
if not cursor:
return
if cursor in seen: # catches a stuck cursor AND any cycle
raise RuntimeError(f"cursor stopped advancing at {cursor}")
seen.add(cursor)
raise RuntimeError(f"hit the {max_calls}-call cap, resume from {cursor}")
total = 0
for rows in paginate("user/followers", "users", username="naval"):
total += len(rows)
print(total)Retries that do not cost you money
Because billing is per call, a retry policy is a spending policy. Retry a server error, never retry a client error, and put a timeout on every request so a hung connection cannot stall a job silently.
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
# Retry 5xx with backoff. Never retry a 4xx: a 401 is a bad key and a 404
# is an account that does not exist, and repeating either just costs money.
retry = Retry(
total=3,
backoff_factor=0.5,
status_forcelist=[500, 502, 503, 504],
allowed_methods=["GET"],
)
session.mount("https://", HTTPAdapter(max_retries=retry))
try:
profile = get("user/info", username="naval")
except requests.HTTPError as exc:
if exc.response.status_code == 401:
raise SystemExit("Key rejected. Check TWITTERAPIS_KEY.")
raiseWhere to go next
The same client shape in Node and Go, or all six languages together on the language clients page. For the endpoint you are about to call, the search API and followers API pages cover parameters and response shapes, and the Tweepy comparison covers what changes if you are moving off it.
Frequently Asked Questions
No. The client above is the whole thing: a requests session with one header set on it, plus a function that builds the URL and decodes the JSON. Because the header never changes and there is no token to refresh, attaching it to the session once means every subsequent call is authenticated without you thinking about it again.
Read the cursor field from each response and pass it back as the cursor parameter on the next call, stopping when the response no longer carries one. The generator on this page does exactly that, so a caller writes an ordinary for loop and never touches cursor handling. Each page is one billed call, so wrap the loop in a page limit when you are testing.
Read it from the environment, as the client above does with os.environ. A key committed to a repository is a key you must rotate, and a key interpolated into a notebook cell tends to end up in the notebook output. For a scheduled job, put it in whatever secret store your runner already has rather than in the job definition.
Tweepy is a wrapper around the official X API, so it inherits that API's pricing, its OAuth flow and its rate windows. It cannot be pointed at a different provider by swapping a base URL, because the endpoints and response shapes differ. Migrating usually means replacing the handful of Tweepy calls you actually use with the get function above, which is less work than the word migration suggests.
Only if you are fetching many independent resources at once. For a sequential job, requests is simpler and the bottleneck is the network rather than the client. If you do need concurrency, httpx with an AsyncClient takes the same header and the same URLs, so the client function converts almost line for line.
The same as from anywhere else: $0.0008 for a standard read call, billed per call with no plan minimum. New accounts start with $0.50 of credit, about 625 read calls, which is enough to page a real follower list before you commit to a budget. The client makes no hidden requests, so the number of calls you are billed for is the number of times your code calls get. Pagination is the case to watch, because one loop over a large follower list is one call per page.