Last Updated: June 01, 2026
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!
Python developer and educator with 15+ years building production systems across data engineering, web APIs, and AI tooling. Founder of Python How To Program — 270+ in-depth tutorials covering the modern Python stack.
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 Get CPU Core Usage with psutil in Python
Intermediate
Your server is running slow, but top shows average CPU at 45% — nothing alarming. Then a colleague points out that core 3 has been pinned at 100% for the last hour while the other seven cores sit idle. A single-threaded bottleneck is strangling your app, invisible to anyone watching only the aggregate number. This is exactly the kind of problem you cannot catch without per-core monitoring, and Python makes it surprisingly easy to build.
The psutil library gives you cross-platform access to CPU usage per core, per-core clock frequency, per-core time breakdowns (user, system, idle), and memory statistics — all in a few lines of Python. It works identically on Windows, macOS, and Linux without requiring root access or system-specific tools like top, htop, or Task Manager. Install it once with pip and you are ready to go.
In this article we will cover everything you need to build a CPU monitoring tool with psutil. We start with a Quick Example so you get per-core numbers immediately. Then we dig into cpu_percent(), physical vs logical core counts, per-core frequency with cpu_freq(), time breakdowns with cpu_times(), memory monitoring, and threshold-based alerting. By the end you will have a real-time terminal dashboard you can point at any machine.
Getting Per-Core CPU Usage: Quick Example
Let us start with the most useful function in psutil for this task. The key is the percpu=True flag on cpu_percent() — without it you get one aggregate number; with it you get a list of percentages, one per logical core.
# quick_cpu_check.py
import psutil
import time
# Pass interval=1 to measure over a 1-second window (recommended)
# percpu=True returns a list -- one value per logical CPU core
core_usage = psutil.cpu_percent(interval=1, percpu=True)
print(f"Logical cores detected: {len(core_usage)}")
print()
for i, pct in enumerate(core_usage):
bar = "#" * int(pct / 5)
print(f" Core {i:>2}: {pct:5.1f}% [{bar:<20}]")
print()
print(f" Overall: {psutil.cpu_percent(interval=None):.1f}%")
Output:
Logical cores detected: 8
Core 0: 23.4% [#### ]
Core 1: 8.1% [# ]
Core 2: 91.3% [################## ]
Core 3: 6.2% [# ]
Core 4: 12.7% [## ]
Core 5: 9.4% [# ]
Core 6: 17.6% [### ]
Core 7: 5.0% [# ]
Overall: 21.7%
The output instantly reveals that core 2 is at 91% while the overall average looks benign at 21.7%. That discrepancy is exactly what aggregate monitoring misses. The interval=1 parameter tells psutil to collect a sample, wait one second, collect another, and return the difference -- this gives you a meaningful measurement rather than a snapshot that could be zero. The len(core_usage) check tells you how many logical cores the machine has, which varies from 2 on a budget laptop to 128 on a high-end server.
The rest of this article explains how each piece works, adds frequency and memory data, and builds toward a live refreshing terminal dashboard. Read on for the details, or jump straight to the Real-Life Example if you want the full script now.
What is psutil and Why Use It?
psutil (process and system utilities) is a cross-platform library for retrieving information on running processes and system utilization -- CPU, memory, disks, network, and sensors. It wraps the underlying OS interfaces (/proc on Linux, sysctl on macOS, Win32 API on Windows) so your Python code runs unchanged on all three platforms.
The alternative to psutil is platform-specific shell commands: mpstat -P ALL 1 on Linux, sysctl hw.perflevel0.physicalcpu on macOS, or WMI queries on Windows. You could parse their output with subprocess, but you would need separate code paths for each OS and your script would break every time the command output format changes. psutil solves all of that.
| Method | Platform | Root Required | Per-Core Data | Python API |
|---|---|---|---|---|
| psutil | Windows / macOS / Linux | No | Yes | Yes -- clean objects |
| mpstat | Linux only | No | Yes | Parse subprocess output |
| top / htop | Unix-like | No | Yes | No -- interactive only |
| WMI | Windows only | Admin for some | Partial | Via pywin32 |
| /proc/stat | Linux only | No | Yes | Manual file parsing |
Install psutil with pip -- it has no dependencies and compiles quickly:
# install_psutil.sh
pip install psutil
Once installed you can import it and immediately start querying system metrics. The sections below walk through each function you need for CPU monitoring.
Logical vs Physical Cores: What cpu_count() Returns
Before diving deeper into usage numbers, it helps to understand what "core" actually means here. Modern CPUs expose more logical cores than they have physical cores because of hyperthreading (Intel) or SMT (AMD). A 4-core chip with hyperthreading shows up as 8 logical cores. psutil lets you query both counts.
# core_count.py
import psutil
logical = psutil.cpu_count(logical=True) # includes hyperthreads
physical = psutil.cpu_count(logical=False) # physical cores only
print(f"Physical cores: {physical}")
print(f"Logical cores: {logical}")
print(f"Hyperthreading: {'Yes' if logical > physical else 'No'}")
print(f"HT ratio: {logical // physical}x" if physical else "")
Output:
Physical cores: 4
Logical cores: 8
Hyperthreading: Yes
HT ratio: 2x
The number of items in the list returned by cpu_percent(percpu=True) always matches cpu_count(logical=True) -- you get one entry per logical core. Physical core count matters for workloads that benefit from true parallelism (CPU-bound Python processes, for example) vs workloads that are mostly I/O-bound and can share a core fine. Knowing the physical count also helps you interpret the per-core usage: if logical cores 0 and 1 are both busy, that is likely one physical core under full load.
Per-Core Frequency with cpu_freq()
CPU frequency tells you whether a core is running at full speed or has been throttled by thermal limits. Modern processors use dynamic frequency scaling: they boost above the rated speed when the workload demands it (and the chip is cool enough), and throttle down to save power or prevent overheating.
# cpu_frequency.py
import psutil
# percpu=True returns a list of scpufreq namedtuples
freqs = psutil.cpu_freq(percpu=True)
if freqs:
print(f"{'Core':<8} {'Current MHz':>12} {'Min MHz':>10} {'Max MHz':>10}")
print("-" * 44)
for i, f in enumerate(freqs):
print(f"Core {i:<3} {f.current:>10.0f} {f.min:>9.0f} {f.max:>9.0f}")
else:
# Some Linux VMs do not expose per-core frequency
overall = psutil.cpu_freq()
print(f"Per-core freq not available. Overall: {overall.current:.0f} MHz")
Output:
Core Current MHz Min MHz Max MHz
--------------------------------------------
Core 0 3600 800 4200
Core 1 4100 800 4200
Core 2 4200 800 4200
Core 3 3200 800 4200
Core 4 3800 800 4200
Core 5 4000 800 4200
Core 6 4200 800 4200
Core 7 2900 800 4200
A core sitting at its maximum frequency (4200 MHz here) that also shows high CPU usage is healthy -- it is working hard and boosting as designed. A core showing high CPU usage but stuck at minimum frequency (800 MHz) is likely being throttled due to heat, and you have a cooling problem rather than a workload problem. The defensive check for if freqs: is important: some virtualized Linux environments do not expose per-core frequency and return an empty list.
Per-Core Time Breakdown with cpu_times()
CPU usage percentage tells you HOW MUCH a core is working, but not what it is doing. cpu_times() breaks the time a CPU has spent into categories: user space (your code), kernel space (system calls), idle, and on Linux you also get I/O wait and steal time (from hypervisor overhead in VMs).
# cpu_times_breakdown.py
import psutil
times = psutil.cpu_times(percpu=True)
print(f"{'Core':<6} {'User%':>7} {'Sys%':>7} {'Idle%':>7} {'IOWait%':>9}")
print("-" * 40)
for i, t in enumerate(times):
total = t.user + t.system + t.idle + getattr(t, 'iowait', 0.0)
if total == 0:
continue
user_pct = t.user / total * 100
sys_pct = t.system / total * 100
idle_pct = t.idle / total * 100
iowait_pct = getattr(t, 'iowait', 0.0) / total * 100
print(f"Core {i:<1} {user_pct:>7.1f} {sys_pct:>7.1f} {idle_pct:>7.1f} {iowait_pct:>9.1f}")
Output:
Core User% Sys% Idle% IOWait%
----------------------------------------
Core 0 18.2 4.1 77.7 0.0
Core 1 6.5 1.6 91.9 0.0
Core 2 88.4 2.9 8.7 0.0
Core 3 5.1 1.1 93.8 0.0
Core 4 11.3 0.8 87.9 0.1
Core 5 8.7 0.9 90.4 0.0
Core 6 15.2 1.4 83.4 0.0
Core 7 4.2 0.6 95.2 0.0
Note the getattr(t, 'iowait', 0.0) pattern. The iowait field only exists on Linux; using getattr with a default keeps the code portable to macOS and Windows. A core with high user% is running application code. High sys% means lots of system calls (file I/O, socket operations). High iowait% means the core is waiting on storage -- often a sign that your database or file access is the real bottleneck, not CPU.
Memory Monitoring: virtual_memory()
CPU monitoring is rarely useful in isolation -- memory pressure often causes CPU spikes as the OS spends cycles on swapping. Adding memory data to your monitor gives a more complete picture.
# memory_check.py
import psutil
mem = psutil.virtual_memory()
swap = psutil.swap_memory()
def fmt_bytes(n):
for unit in ('B', 'KB', 'MB', 'GB', 'TB'):
if n < 1024:
return f"{n:.1f} {unit}"
n /= 1024
return f"{n:.1f} PB"
print("RAM:")
print(f" Total: {fmt_bytes(mem.total)}")
print(f" Available: {fmt_bytes(mem.available)}")
print(f" Used: {fmt_bytes(mem.used)} ({mem.percent:.1f}%)")
print(f" Buffers: {fmt_bytes(getattr(mem, 'buffers', 0))}")
print(f" Cached: {fmt_bytes(getattr(mem, 'cached', 0))}")
print()
print("Swap:")
print(f" Total: {fmt_bytes(swap.total)}")
print(f" Used: {fmt_bytes(swap.used)} ({swap.percent:.1f}%)")
Output:
RAM:
Total: 15.9 GB
Available: 9.3 GB
Used: 6.1 GB (38.7%)
Buffers: 312.0 MB
Cached: 4.2 GB
Swap:
Total: 2.0 GB
Used: 0.0 MB (0.0%)
The mem.available field is the most actionable metric here -- it is not the same as mem.total - mem.used. Available includes memory that is currently used for caches but can be reclaimed immediately by applications. If mem.available drops near zero while swap.percent climbs, your machine is under genuine memory pressure and performance will degrade. The getattr calls on buffers and cached guard against Windows, which does not expose those fields.
Threshold Alerting: Raising Warnings When Cores Spike
Collecting metrics is only useful if something reacts to them. The next step is adding threshold checks so your monitoring code can trigger an alert, write to a log file, or send a notification when a core crosses a usage limit you define.
# cpu_alerts.py
import psutil
import time
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%H:%M:%S",
)
CPU_WARN_PCT = 70.0 # warn if any single core exceeds this
CPU_CRIT_PCT = 90.0 # critical if any core exceeds this
MEM_WARN_PCT = 80.0 # warn if RAM usage exceeds this
CHECK_INTERVAL = 5 # seconds between checks
def check_once():
per_core = psutil.cpu_percent(interval=1, percpu=True)
mem = psutil.virtual_memory()
for i, pct in enumerate(per_core):
if pct >= CPU_CRIT_PCT:
logging.critical("Core %d at %.1f%% -- CRITICAL", i, pct)
elif pct >= CPU_WARN_PCT:
logging.warning("Core %d at %.1f%% -- high usage", i, pct)
if mem.percent >= MEM_WARN_PCT:
logging.warning("RAM at %.1f%% -- available: %.1f GB",
mem.percent, mem.available / 1e9)
if __name__ == "__main__":
logging.info("Starting CPU/memory monitor (Ctrl+C to stop)")
try:
while True:
check_once()
time.sleep(CHECK_INTERVAL)
except KeyboardInterrupt:
logging.info("Monitor stopped.")
Output:
09:14:01 [INFO] Starting CPU/memory monitor (Ctrl+C to stop)
09:14:02 [WARNING] Core 2 at 73.5% -- high usage
09:14:07 [CRITICAL] Core 2 at 94.1% -- CRITICAL
09:14:12 [CRITICAL] Core 2 at 98.7% -- CRITICAL
09:14:17 [INFO] Monitor stopped.
Using the standard logging module rather than print() means you can redirect this output to a file with one line change (filename="monitor.log" in the basicConfig call), or hook it into any structured logging pipeline. The CHECK_INTERVAL constant separated from cpu_percent(interval=1) is intentional -- the interval on cpu_percent controls measurement accuracy, while CHECK_INTERVAL controls how often you act on the results.
Real-Life Example: Live Terminal CPU Dashboard
Let us combine everything into a dashboard that refreshes in place every two seconds, showing per-core bars, frequency, and memory -- all in one compact terminal view.
# cpu_dashboard.py
import psutil
import time
import os
CPU_WARN = 70.0
CPU_CRIT = 90.0
REFRESH = 2.0 # seconds between refreshes
def color(pct):
"""Return ANSI color code based on usage percentage."""
if pct >= CPU_CRIT:
return "\033[91m" # bright red
if pct >= CPU_WARN:
return "\033[93m" # yellow
return "\033[92m" # green
RESET = "\033[0m"
def make_bar(pct, width=24):
filled = int(pct / 100 * width)
return "#" * filled + "-" * (width - filled)
def render():
os.system("cls" if os.name == "nt" else "clear")
print("=" * 56)
print(" psutil CPU Dashboard -- press Ctrl+C to exit")
print("=" * 56)
per_core = psutil.cpu_percent(interval=1, percpu=True)
freqs = psutil.cpu_freq(percpu=True) or []
mem = psutil.virtual_memory()
logical = psutil.cpu_count(logical=True)
physical = psutil.cpu_count(logical=False)
print(f" Cores: {physical} physical / {logical} logical\n")
for i, pct in enumerate(per_core):
freq_str = ""
if i < len(freqs):
freq_str = f" {freqs[i].current:>5.0f} MHz"
bar = make_bar(pct)
c = color(pct)
print(f" Core {i:>2}: {c}[{bar}]{RESET} {pct:5.1f}%{freq_str}")
avg = sum(per_core) / len(per_core) if per_core else 0
print(f"\n Avg: [{make_bar(avg)}] {avg:5.1f}%")
print()
mem_bar = make_bar(mem.percent, width=24)
mc = color(mem.percent)
avail_gb = mem.available / 1e9
print(f" RAM: {mc}[{mem_bar}]{RESET} {mem.percent:5.1f}% "
f"({avail_gb:.1f} GB free)")
swap = psutil.swap_memory()
if swap.total > 0:
swap_bar = make_bar(swap.percent, width=24)
sc = color(swap.percent)
print(f" Swap: {sc}[{swap_bar}]{RESET} {swap.percent:5.1f}%")
print()
print(f" Updated every {REFRESH}s -- {time.strftime('%H:%M:%S')}")
print("=" * 56)
if __name__ == "__main__":
try:
while True:
render()
time.sleep(REFRESH)
except KeyboardInterrupt:
print("\nDashboard stopped.")
Output (sample frame):
========================================================
psutil CPU Dashboard -- press Ctrl+C to exit
========================================================
Cores: 4 physical / 8 logical
Core 0: [######------------------] 25.4% 3600 MHz
Core 1: [#-----------------------] 8.1% 2900 MHz
Core 2: [######################--] 91.3% 4200 MHz
Core 3: [#-----------------------] 6.2% 3100 MHz
Core 4: [###---------------------] 12.7% 3400 MHz
Core 5: [##----------------------] 9.4% 3200 MHz
Core 6: [###---------------------] 17.6% 3800 MHz
Core 7: [#-----------------------] 5.0% 2800 MHz
Avg: [####--------------------] 22.0%
RAM: [############------------] 51.2% (7.8 GB free)
Swap: [------------------------] 0.0%
Updated every 2s -- 09:17:44
========================================================
The os.system("cls" if os.name == "nt" else "clear") call clears the terminal before each refresh, giving the appearance of an in-place update rather than scrolling output. The ANSI color codes turn critical cores red and high-usage cores yellow in any terminal that supports them (macOS Terminal, Linux terminals, Windows Terminal). To log to a file instead of the terminal, replace the render() call with the check_once() pattern from the alerting section. You can also extend this script by adding disk I/O stats with psutil.disk_io_counters(perdisk=True) or network throughput with psutil.net_io_counters(pernic=True).
Frequently Asked Questions
Why does cpu_percent() return 0.0 when I call it with no arguments?
The first call to psutil.cpu_percent() with no interval and no previous call in the same process always returns 0.0. psutil calculates CPU usage as the difference between two samples taken some time apart. The first call just sets the baseline; the second call (or a call with interval=N) returns the actual measurement. Always use interval=1 (or at least 0.1) for accurate readings, or call the function once at startup to prime it and then call it again after a small sleep.
When should I use logical=True vs logical=False in cpu_count()?
Use cpu_count(logical=True) when you want to know how many workers to create for I/O-bound tasks -- more logical cores means more threads can be useful. Use cpu_count(logical=False) for CPU-bound work where you spawn Python processes -- extra logical cores from hyperthreading rarely help CPU-bound code and can actually hurt throughput by competing for the same physical core resources. When in doubt, benchmark both: run your workload with physical workers and with logical workers and compare wall-clock time.
Does psutil need root/admin privileges?
No -- reading CPU usage percentages, frequencies, core counts, and memory stats does not require elevated permissions on Windows, macOS, or Linux. Some psutil functions DO require root, such as reading per-process memory maps or certain sensor temperatures (psutil.sensors_temperatures()). For a pure CPU and memory monitoring script like the one in this article, you can run as a regular user. If you get a psutil.AccessDenied exception, check which specific function triggered it -- it is almost certainly a process-level function, not a system-level one.
cpu_freq(percpu=True) returns an empty list on my Linux VM. What is wrong?
This is expected behavior on many virtualized Linux environments. The guest OS does not always have access to the host CPU's frequency scaling information. The psutil.cpu_freq() function reads from /sys/devices/system/cpu/cpu*/cpufreq/ on Linux, which may not be populated by the hypervisor. Some cloud VMs (AWS, GCP, Azure) intentionally withhold this data. The safe approach is to always check if freqs: before iterating, and fall back to a single aggregate call (psutil.cpu_freq(percpu=False)) or simply skip the frequency column. The CPU usage percentage from cpu_percent() remains accurate even when frequency data is unavailable.
Does this code work on Windows without any changes?
Yes, with one small caveat: the ANSI color codes in the dashboard script require Windows 10 version 1607 or later with Windows Terminal or a VT100-compatible terminal. The standard Windows Command Prompt (cmd.exe) on older Windows versions does not render ANSI codes and will display them as literal characters like [91m. You can guard against this by wrapping the ANSI output in a try/except or by using the colorama library (pip install colorama), which translates ANSI codes to Win32 console calls. Everything else -- cpu_percent(), cpu_count(), cpu_freq(), virtual_memory(), and swap_memory() -- works identically on Windows.
Can I get CPU temperature with psutil?
On Linux and some macOS hardware, yes: psutil.sensors_temperatures() returns a dictionary of sensor readings grouped by device name. The key for CPU cores is usually 'coretemp' or 'k10temp' depending on the chip. Each entry has current, high, and critical temperature values in Celsius. This function is not available on Windows -- psutil simply does not expose it there because the Windows thermal sensor APIs require platform-specific third-party libraries. On unsupported platforms the call raises AttributeError, so always check hasattr(psutil, 'sensors_temperatures') before using it.
Conclusion
psutil makes per-core CPU monitoring a matter of two function calls. cpu_percent(interval=1, percpu=True) gives you a list of usage values -- one per logical core -- that reveals the imbalances a single aggregate number would hide. cpu_count(logical=True/False) tells you whether extra cores come from hyperthreading or are genuine physical cores. cpu_freq(percpu=True) shows whether cores are boosting or being throttled. cpu_times(percpu=True) breaks usage down into user, system, and iowait time so you know whether CPU cycles are spent on application code, kernel calls, or waiting on storage. And virtual_memory() and swap_memory() round out the picture by capturing memory pressure alongside CPU load.
Extend the dashboard by adding psutil.disk_io_counters(perdisk=True) for storage throughput, psutil.net_io_counters(pernic=True) for network stats, or hook the alert thresholds into a notification service like Slack or PagerDuty. You could also export metrics to a time-series database like Prometheus by wrapping the psutil calls in a Flask endpoint and adding a Prometheus client. The psutil documentation at psutil.readthedocs.io covers every available function in depth.
For deeper exploration, the Python Scalene profiler article shows how to go beyond monitoring into detailed line-level CPU and memory profiling within your own code, and the Python task automation guide covers scheduling monitoring scripts to run on a cron job.
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.
Related Articles
- How To Build a Telegram Bot with Python
- How To Build a REST API with FastAPI in Python
- How To Build a Discord Bot With Python Using discord.py
- How To Build a Telegram Bot With Python Using python-telegram-bot
- How To Build a Python gRPC Service with grpcio
- How To Build REST APIs with Python and Starlette
Continue Learning Python
Tutorials you might also find useful:
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.