Planet Python
Last update: May 02, 2025 07:27 PM UTC
May 02, 2025
Eli Bendersky
Notes on implementing Attention
Some notes on implementing attention blocks in pure Python + Numpy. The focus here is on the exact implementation in code, explaining all the shapes throughout the process. The motivation for why attention works is not covered here too deeply - there are plenty of excellent online resources explaining it.
Several papers are mentioned throughout the code; they are:
- AIAYN - Attention Is All You Need by Vaswani et al.
- GPT-3 - Language Models are Few-Shot Learners by Brown et al.
Basic scaled self-attention
We'll start with the most basic scaled dot product self-attention, working on a single sequence of tokens, without masking.
The input is a 2D array of shape (N, D). N is the length of the sequence (how many tokens it contains) and D is the embedding depth - the length of the embedding vector representing each token [1]. D could be something like 512, or more, depending on the model.

A self-attention module is parameterized with three weight matrices, Wk, Wq and Wv. Some variants also have accompanying bias vectors, but the AIAYN paper doesn't use them, so I'll skip them here. In the general case, the shape of each weight matrix is (D, HS), where HS is some fraction of D. HS stands for "head size" and we'll see what this means soon. This is a diagram of a self-attention module (the diagram assumes N=6, D is some large number and so is HS). In the diagram, @ stands for matrix multiplication (Python/Numpy syntax):

Here's a basic Numpy implementation of this:
# self_attention the way it happens in the Transformer model. No bias.
# D = model dimension/depth (length of embedding)
# N = input sequence length
# HS = head size
#
# x is the input (N, D), each token in a row.
# Each of W* is a weight matrix of shape (D, HS)
# The result is (N, HS)
def self_attention(x, Wk, Wq, Wv):
# Each of these is (N, D) @ (D, HS) = (N, HS)
q = x @ Wq
k = x @ Wk
v = x @ Wv
# kq: (N, N) matrix of dot products between each pair of q and k vectors.
# The division by sqrt(HS) is the scaling.
kq = q @ k.T / np.sqrt(k.shape[1])
# att: (N, N) attention matrix. The rows become the weights that sum
# to 1 for each output vector.
att = softmax_lastdim(kq)
return att @ v # (N, HS)
The "scaled" part is just dividing kq by the square root of HS, which is done to keep the values of the dot products manageable (otherwise they would grow with the size of the contracted dimension).
The only dependency is a function for calculating Softmax across the last dimension of an input array:
def softmax_lastdim(x):
"""Compute softmax across last dimension of x.
x is an arbitrary array with at least two dimensions. The returned array has
the same shape as x, but its elements sum up to 1 across the last dimension.
"""
# Subtract the max for numerical stability
ex = np.exp(x - np.max(x, axis=-1, keepdims=True))
# Divide by sums across last dimension
return ex / np.sum(ex, axis=-1, keepdims=True)
When the input is 2D, the "last dimension" is the columns. Colloquially, this Softmax function acts on each row of x separately; it applies the Softmax formula to the elements (columns) of the row, ending up with a row of numbers in the range [0,1] that all sum up to 1.
Another note on the dimensions: it's possible for the Wv matrix to have a different second dimension from Wq and Wk. If you look at the diagram, you can see this will work out, since the softmax produces (N, N), and whatever the second dimension of V is, will be the second dimension of the output. The AIAYN paper designates these dimensions as d_k and d_v, but in practice d_k=d_v in all the variants it lists. I found that these dimensions are typically the same in other papers as well. Therefore, for simplicity I just made them all equal to D in this post; if desired, a variant with different d_k and d_v is a fairly trivial modification to this code.
Batched self-attention
In the real world, the input array is unlikely to be 2D because models are trained on batches of input sequences. To leverage the parallelism of modern hardware, whole batches are typically processed in the same operation.

The batched version of scaled self-attention is very similar to the non-batched one, due to the magic of Numpy matrix multiplication and broadcasts. Now the input shape is (B, N, D), where B is the batch dimension. The W* matrices are still (D, HS); multiplying a (B, N, D) array by (D, HS) performs contraction between the last axis of the first array and the first axis of the second array, resulting in (B, N, HS). Here's the code, with the dimensions annotated for each operation:
# self_attention with inputs that have a batch dimension.
# x has shape (B, N, D)
# Each of W* has shape (D, D)
def self_attention_batched(x, Wk, Wq, Wv):
q = x @ Wq # (B, N, HS)
k = x @ Wk # (B, N, HS)
v = x @ Wv # (B, N, HS)
kq = q @ k.swapaxes(-2, -1) / np.sqrt(k.shape[-1]) # (B, N, N)
att = softmax_lastdim(kq) # (B, N, N)
return att @ v # (B, N, HS)
Note that the only difference between this and the non-batched version is the line calculating kq:
- Since k is no longer 2D, the notion of "transpose" is ambiguous so we explicitly ask to swap the last and the penultimate axis, leaving the first axis (B) intact.
- When calculating the scaling factor we use k.shape[-1] to select the last dimension of k, instead of k.shape[1] which only selects the last dimension for 2D arrays.
In fact, this function could also calculate the non-batched version! From now on, we'll assume that all inputs are batched, and all operations are implicitly batched. I'm not going to be using the "batched" prefix or suffix on functions any more.
The basic underlying idea of the attention module is to shift around the multi-dimensional representations of tokens in the sequence towards a better representation of the entire sequence. The tokens attend to each other. Specifically, the matrix produced by the Softmax operation is called the attention matrix. It's (N, N); for each token it specifies how much information from every other token in the sequence should be taken into account. For example, a higher number in cell (R, C) means that there's a stronger relation of token at index R in the sequence to the token at index C.
Here's a nice example from the AIAYN paper, showing a word sequence and the weights produced by two attention heads (purple and brown) for a given position in the input sequence:

This shows how the model is learning to resolve what the word "its" refers to in the sentence. Let's take just the purple head as an example. The index of token "its" in the sequence is 8, and the index of "Law" is 1. In the attention matrix for this head, the value at index (8, 1) will be very high (close to 1), with other values in the same row much lower.
While this intuitive explanation isn't critical to understand how attention is implemented, it will become more important when we talk about masked self-attention later on.
Multi-head attention
The attention mechanism we've seen so far has a single set of K, Q and V matrices. This is called one "head" of attention. In today's models, there are typically multiple heads. Each head does its attention job separately, and in the end all these results are concatenated and feed through a linear layer.
In what follows, NH is the number of heads and HS is the head size. Typically, NH times HS would be D; for example, the AIAYN paper mentions several configurations for D=512: NH=8 and HS=64, NH=32 and HS=16, and so on [2]. However, the math works out even if this isn't the case, because the final linear ("projection") layer maps the output back to (N, D).
Assuming the previous diagram showing a self-attention module is a single head with input (N, D) and output (N, HS), this is how multiple heads are combined:

Each of the (NH) heads has its own parameter weights for Q, K and V. Each attention head outputs a (N, HS) matrix; these are concatenated along the last dimension to (N, NH * HS), which is passed through a final linear projection.
Here's a function implementing (batched) multi-head attention; for now, please ignore the code inside do_mask conditions:
# x has shape (B, N, D)
# In what follows:
# NH = number of heads
# HS = head size
# Each W*s is a list of NH weight matrices of shape (D, HS).
# Wp is a weight matrix for the final linear projection, of shape (NH * HS, D)
# The result is (B, N, D)
# If do_mask is True, each attention head is masked from attending to future
# tokens.
def multihead_attention_list(x, Wqs, Wks, Wvs, Wp, do_mask=False):
# Check shapes.
NH = len(Wks)
HS = Wks[0].shape[1]
assert len(Wks) == len(Wqs) == len(Wvs)
for W in Wqs + Wks + Wvs:
assert W.shape[1] == HS
assert Wp.shape[0] == NH * HS
# List of head outputs
head_outs = []
if do_mask:
# mask is a lower-triangular (N, N) matrix, with zeros above
# the diagonal and ones on the diagonal and below.
N = x.shape[1]
mask = np.tril(np.ones((N, N)))
for Wk, Wq, Wv in zip(Wks, Wqs, Wvs):
# Calculate self attention for each head separately
q = x @ Wq # (B, N, HS)
k = x @ Wk # (B, N, HS)
v = x @ Wv # (B, N, HS)
kq = q @ k.swapaxes(-2, -1) / np.sqrt(k.shape[-1]) # (B, N, N)
if do_mask:
# Set the masked positions to -inf, to ensure that a token isn't
# affected by tokens that come after it in the softmax.
kq = np.where(mask == 0, -np.inf, kq)
att = softmax_lastdim(kq) # (B, N, N)
head_outs.append(att @ v) # (B, N, HS)
# Concatenate the head outputs and apply the final linear projection
all_heads = np.concatenate(head_outs, axis=-1) # (B, N, NH * HS)
return all_heads @ Wp # (B, N, D)
It is possible to vectorize this code even further; you'll sometimes see the heads laid out in a separate (4th) dimension instead of being a list. See the Vectorizing across the heads dimension section.
Masked (or Causal) self-attention
Attention modules can be used in both encoder and decoder blocks. Encoder blocks are useful for things like language understanding or translation; for these, it makes sense for each token to attend to all the other tokens in the sequence.
However, for generative models this presents a problem: if during training a word attends to future words, the model will just "cheat" and not really learn how to generate the next word from only past words. This is done in a decoder block, and for this we need to add masking to attention.
Conceptually, masking is very simple. Consider the sentence:
People like watching funny cat videos
When our attention code generates the att matrix, it's a square (N, N) matrix with attention weights from each token to each other token in the sequence:

What we want is for all the gray cells in this matrix to be zero, to ensure that a token doesn't attend to future tokens. The blue cells in the matrix add up to 1 in each row, after the softmax operation.
Now take a look at the previous code sample and see what happens when do_mask=True:
- First, a (N, N) lower-triangular array is prepared with zeros above the diagonal and ones on the diagonal and below.
- Then, before we pass the scaled QK^T to softmax, we set its values to -\infty wherever the mask matrix is 0. This ensures that the softmax function will assign zeros to outputs at these indices, while still producing the proper values in the rest of the row.
Another name for masked self-attention is causal self-attention. This is a very good name that comes from causal systems in control theory.
Intuition - what attention does
What does the attention block try to accomplish? To think about it intuitively, let's focus on a single token in the input (ignoring batch) - x[i]. For this token, the attention block produces an output token out[i] that blends x[i]'s embedding (multi-dimensional dense vector representation) with contextual information from all the tokens preceding it in the sequence, i.e. x[:i].
The way this is done is first calculating the query vector q for x[i] (using Wq). This query can be thought of as "what attributes does this token care about in its context tokens".
Then, for each of the context tokens (including x[i] itself) we calculate:
- Key (using Wk): these are the attributes of the token that queries may refer to.
- Value (using Wv): these are the associated values tokens carry.
When attention calculates q @ K.T for each token, the result is - for each context token - the weights to use for mixing in the token's value. Then, when this is multiplied by V, the values are properly weighted.
So this is a very general approach for the model to learn what kind of information each token "cares" about in its context tokens, and how to blend the token's embedding with those of the preceding context tokens, to properly encode the context the token is encountered in.
Our implementation, starting with the basic scaled self-attention, implements this for all tokens in the input sequence simultaneously; hence, we don't just take a single x[i], calculate its q and then multiply that by K.T. Rather, we calculate Q from all x, and continue using matrix multiplications to vectorize these calculations across the entire sequence.
It's important to keep in mind that this intuitive explanation suffers from anthropomorphism. We try to explain what the model does intuitively, but in reality this is only a very abstract approximation of what's happening (consider that attention has multiple heads, and also that that LLMs typically have dozens of repeating transformer layers with self-attention blocks, applying the same mechanism over and over again).
Cross-attention
So far we've been working with self-attention blocks, where the self suggests that elements in the input sequence attend to other elements in the same input sequence.
Another variant of attention is cross-attention, where elements of one sequence attend to elements in another sequence. This variant exists in the decoder block of the AIAYN paper. This is a single head of cross-attention:

Here we have two sequences with potentially different lengths: xq and xv. xq is used for the query part of attention, while xv is used for the key and value parts. The rest of the dimensions remain as before. The output of such a block is shaped (Nq, HS).
This is an implementation of multi-head cross-attention; it doesn't include masking, since masking is not typically necessary in cross attention - it's OK for elements of xq to attend to all elements of xv [3]:
# Cross attention between two input sequences that can have different lengths.
# xq has shape (B, Nq, D)
# xv has shape (B, Nv, D)
# In what follows:
# NH = number of heads
# HS = head size
# Each W*s is a list of NH weight matrices of shape (D, HS).
# Wp is a weight matrix for the final linear projection, of shape (NH * HS, D)
# The result is (B, Nq, D)
def multihead_cross_attention_list(xq, xv, Wqs, Wks, Wvs, Wp):
# Check shapes.
NH = len(Wks)
HS = Wks[0].shape[1]
assert len(Wks) == len(Wqs) == len(Wvs)
for W in Wqs + Wks + Wvs:
assert W.shape[1] == HS
assert Wp.shape[0] == NH * HS
# List of head outputs
head_outs = []
for Wk, Wq, Wv in zip(Wks, Wqs, Wvs):
q = xq @ Wq # (B, Nq, HS)
k = xv @ Wk # (B, Nv, HS)
v = xv @ Wv # (B, Nv, HS)
kq = q @ k.swapaxes(-2, -1) / np.sqrt(k.shape[-1]) # (B, Nq, Nv)
att = softmax_lastdim(kq) # (B, Nq, Nv)
head_outs.append(att @ v) # (B, Nq, HS)
# Concatenate the head outputs and apply the final linear projection
all_heads = np.concatenate(head_outs, axis=-1) # (B, Nq, NH * HS)
return all_heads @ Wp # (B, Nq, D)
Vectorizing across the heads dimension
The multihead_attention_list implementation shown above uses lists of weight matrices as input. While this makes the code clearer, it's not a particularly friendly format for an optimized implementation - especially on accelerators like GPUs and TPUs. We can vectorize it further by creating a new dimension for attention heads.
To understand the trick being used, consider a basic matmul of (8, 6) by (6, 2):

Now suppose we want to multiply our LHS by another (6, 2) matrix. We can do it all in the same operation by concatenating the two RHS matrices along columns:

If the yellow RHS block in both diagrams is identical, the green block of the result will be as well. And the violet block is just the matmul of the LHS by the red block of the RHS. This stems from the semantics of matrix multiplication, and is easy to verify on paper.
Now back to our multi-head attention. Note that we multiply the input x by a whole list of weight matrices - in fact, by three lists (one list for Q, one for K, and another for V). We can use the same vectorization technique by concatenating all these weight matrices into a single one. Assuming that NH * HS = D, the shape of the combined matrix is (D, 3 * D). Here's the vectorized implementation:
# x has shape (B, N, D)
# In what follows:
# NH = number of heads
# HS = head size
# NH * HS = D
# W is expected to have shape (D, 3 * D), with all the weight matrices for
# Qs, Ks, and Vs concatenated along the last dimension, in this order.
# Wp is a weight matrix for the final linear projection, of shape (D, D).
# The result is (B, N, D).
# If do_mask is True, each attention head is masked from attending to future
# tokens.
def multihead_attention_vec(x, W, NH, Wp, do_mask=False):
B, N, D = x.shape
assert W.shape == (D, 3 * D)
qkv = x @ W # (B, N, 3 * D)
q, k, v = np.split(qkv, 3, axis=-1) # (B, N, D) each
if do_mask:
# mask is a lower-triangular (N, N) matrix, with zeros above
# the diagonal and ones on the diagonal and below.
mask = np.tril(np.ones((N, N)))
HS = D // NH
q = q.reshape(B, N, NH, HS).transpose(0, 2, 1, 3) # (B, NH, N, HS)
k = k.reshape(B, N, NH, HS).transpose(0, 2, 1, 3) # (B, NH, N, HS)
v = v.reshape(B, N, NH, HS).transpose(0, 2, 1, 3) # (B, NH, N, HS)
kq = q @ k.swapaxes(-1, -2) / np.sqrt(k.shape[-1]) # (B, NH, N, N)
if do_mask:
# Set the masked positions to -inf, to ensure that a token isn't
# affected by tokens that come after it in the softmax.
kq = np.where(mask == 0, -np.inf, kq)
att = softmax_lastdim(kq) # (B, NH, N, N)
out = att @ v # (B, NH, N, HS)
return out.transpose(0, 2, 1, 3).reshape(B, N, D) @ Wp # (B, N, D)
This code computes Q, K and V in a single matmul, and then splits them into separate arrays (note that on accelerators these splits and later transposes may be very cheap or even free as they represent a different access pattern into the same data).
Each of Q, K and V is initially (B, N, D), so they are reshaped into a more convenient shape by first splitting the D into (NH, HS), and finally changing the order of dimensions to get (B, NH, N, HS). In this format, both B and NH are considered batch dimensions that are fully parallelizable. The QK^T computation can then proceed as before, and Numpy will automatically perform the matmul over all the batch dimensions.
Sometimes you'll see an alternative notation used in papers for these matrix multiplications: numpy.einsum. For example, in our last code sample the computation of kq could also be written as:
kq = np.einsum("bhqd,bhkd->bhqk", q, k) / np.sqrt(k.shape[-1])
Code
The full code for these samples, with tests, is available in this repository.
[1] | In LLM papers, D is often called d_{model}. |
[2] | In the GPT-3 paper, this is also true for all model variants. For example, the largest 175B model has NH=96, HS=128 and D=12288. |
[3] | It's also not as easy to define mathematically: how do we make a non-square matrix triangular? And what does it mean when the lengths of the two inputs are different? |
Seth Michael Larson
whichprovides: an abstraction of "yum provides"
This critical role would not be possible without funding from the Alpha-Omega project.
I'm announcing a new small project I've created as a part
of my work on Software Bill-of-Materials for Python packages.
The library is called whichprovides
and it's available
on PyPI under the same name:
$ python -m pip install whichprovides
You can use the tool as a CLI, but many users will be using this library indirectly through tools like auditwheel.
The primary purpose of the library is to reverse a file path
on your system back to the package ecosystem and package
that "provided" the file, similar to how yum provides
works:
$ python -m whichprovides /usr/bin/python3.10
/usr/bin/python3.10: pkg:deb/ubuntu/python3.10-minimal@3.10.12-1~22.04.9
This information allows tools to create package URLs (PURLs) for the files they use for building a Python package. PURLs are useful as a software identifier for performing vulnerability scanning.
Currently, this library supports the following package ecosystems:
- RPM (Red Hat, CentOS, Rocky Linux, AlmaLinux)
- Dpkg (Debian, Ubuntu)
- Aptitude (Ubuntu)
- Apk (Alpine)
I'm interested in adding support for other package ecosystems, too. If you'd like to contribute support for a new package ecosystem or just generally review the code, I'd welcome these types of contributions.
Techiediaries - Django
Generate and Crack Passwords with Python and Ethical Considerations
In modern cybersecurity space, password security remains a critical line of defense against unauthorized access. This post explains Python’s capabilities for both generating secure passwords and simulating password-cracking techniques.
Find Wi-Fi Connected Devices with Python
This article provides a step by step approach to building a Python script for identifying devices connected to a Wi-Fi network. By leveraging Address Resolution Protocol (ARP) scanning and MAC address vendor lookup tools, the solution automates device discovery and enhances network visibility. Below, we explore the theory and steps, and optimization hacks for building a robust network monitoring tool.
Finding Geo Location with Python Using IP and GPS
This article explains five distinct methods for implementing geolocation in Python, providing detailed implementations for both IP-based and GPS-based approaches.
Quansight Labs Blog
Enhancing Developer Experience at SciPy - Intel oneAPI/MSVC Support and Migrating to spin
Highlights the work done to improve developer experience at SciPy, specifically on supporting Intel oneAPI/MSVC and spin
May 01, 2025
Zero to Mastery
[April 2025] Python Monthly Newsletter 🐍
65th issue of Andrei Neagoie's must-read monthly Python Newsletter: Python's New t-strings, The Best Programmers I Know, and much more. Read the full newsletter to get up-to-date with everything you need to know from last month.
Seth Michael Larson
Better boosting on Mastodon with smart clients
Happy May Day (aka International Workers' Day). Consider celebrating by reading about the origins of May Day and how workers before us fought and died for the 8-hour work week that we, the workers of today, now enjoy.
If you've been on Mastodon for long enough it's likely you've heard the phrase “you are the algorithm” in reference to the fact that Mastodon by default doesn't provide algorithmic curation of your timeline. Instead, Mastodon implements a simple linear timeline and users are expected to "boost" posts so they reach a wider audience, specifically their own followers. I'll be calling Mastodon's approach "boost curation" in this post.
Don't get me wrong, I'm a fan of understandable systems and a critic of algorithmic curation. I do wonder if Mastodon's allergy to algorithm curation does not play well for folks who have different expectations of social media, such as wanting more "town-square" moments [1] where a substantial number of users are talking about one or two topics that interest them all at once.
Here are some other downsides of boost curation compared to algorithmic curation:
- Boosts are only applied during your waking hours, meaning your followers in different timezones than you don't ever "benefit" from your curation.
- Boosts immediately put content to the top of feeds, meaning whatever you see during a given interaction with Mastodon is likely to be one or two people boosting tons of posts in succession rather than a blend of boosted posts and new posts from your followers.
- Boosts can mean that a post is seen more than once by a single person over the course of a day. This phenomenon might exacerbate the feeling of there being less content on Mastodon compared to other social media platforms.
- Once a post is no longer being boosted it almost immediately craters because everyone's feeds move on to new content. With algorithmic curation this post would have chances for "second-winds" by being offered to readers outside the linear timeline.
So what can be done about the above issues while maintaining a linear algorithm-less timeline?
Breaking assumptions for smarter boosts
Many other networked systems have methods for clients to "collaborate" and achieve a goal that helps users without actually needing to know intimate details about other implementations (which is impossible in a diverse ecosystem of clients) or to share additional information between clients. An example that comes to mind here is the different TCP congestion control algorithms which collaborate to maximize bandwidth without needing to directly share information.
What if selecting "boost" in a Mastodon client didn't mean the post was reposted immediately to your followers, only that the post would be reposted eventually at the clients' discretion? To keep control in the users' hands, clients could offer an override to repost immediately, like a double-tap?
With this assumption broken, the client is now allowed to be "smart" by choosing the timing when the boost is applied to a post. The following simple logic could be implemented:
- If the post was created recently, wait for a bit to see if the post garners any other boosts. This is done because the post is likely already at the "top" of relevant feeds.
- If the post hadn't been boosted recently and the client hadn't boosted another post recently: boost the post. By only boosting one post at a time the client isn't clogging everyone's feeds with only boosts from a single account.
- If not, delay for some amount of time and reevaluate.
The above basic routine could be refined mostly by how the "delay" is calculated. The delay could be a simple whole number or more complicated, like taking into account information about your followers or recent posts on the same topic. To avoid thundering herds between like-minded clients this delay could be treated almost like a "retry" where a random jitter is applied.
Are there any Mastodon clients that already implement something similar to this that I'm not aware of? What pitfalls or other choices could a client make to make this behavior better? Send me your thoughts on this topic or Mastodon in general.
[1] Note that there are downsides to being a social media town-square, such as context collapse.
April 30, 2025
Juri Pakaste
Updating multiple rows with SQL and avoiding collisions
I ran into an interesting problem with SQL the other day: how do you update multiple rows while maintaining an uniqueness constraint?
I have a table where each row describes an item in an ordered list. Each row has a position
value. They are
integers, not necessarily contiguous but each unique. A larger position
means the row is further down the list. For
reordering the rows, I sometimes need to make space between two positions.
How do you do that? Well, the obvious answer is a simple update statement, assuming you want to make space for one item before position
5:
UPDATE table
SET position = position + 1
WHERE position >= 5
Despite having used SQL since the 90s I don't think I've ever needed to do this before. It seemed simple enough, but I found out the solution I went with is not only obvious but also wrong. When you have contiguous position
values, that statement causes a unique constraint violation, in both SQLite and PostgreSQL. Having transaction isolation doesn't prevent collisions during updates, even if the end state would be valid.
A helpful LLM tried to suggest the broken solution, then a solution that caused a syntax error and then a solution that involved creating a temporary table. After that I went back to searching the web and finally found a mention about negating the values temporarily which sounded like way less hassle than temporary tables.
That worked great. So, to add space for N items before position
X:
- Begin transaction.
- Instead of incrementing the value on each row greater than or equal to X by N, multiply the value by -1 and decrement it by N.
- Multiply each value smaller than zero by -1.
- Commit.
BEGIN TRANSACTION;
UPDATE table
SET position = -position - 1
WHERE position >= 5;
UPDATE table
SET position = -position
WHERE position < 0;
COMMIT;
Real Python
Modern Web Automation With Python and Selenium
Selenium is a web automation tool that allows you to use Python to programmatically interact with dynamic, JavaScript-generated web pages. Your Python Selenium code drives a real browser that you can instruct to fill out forms, click buttons, scrape dynamically generated data, or write automated tests for web applications.
By implementing the Page Object Model (POM) design pattern, you can create clean and scalable automation scripts that are straightforward to read and maintain.
By the end of this tutorial, you’ll understand that:
- Selenium allows you to launch browsers, visit URLs, and interact with web elements.
- Headless browsers let you run scripts without displaying a browser window, which is useful for automation and testing.
- You can target web elements using different locators, such as CSS selectors, XPath, or IDs.
- Explicit waits provide a flexible way to handle dynamic content by waiting for specific conditions.
- The Page Object Model design pattern separates page structure from business logic.
In this tutorial, you’ll learn how to use Selenium with Python to build a fully functional music player that interacts with Bandcamp’s Discover page. You’ll control the player from the command line while a headless Firefox browser runs in the background. With it, you’ll be able to play tracks, pause music, list available tracks, and load more tracks, replicating some of the website’s core functionality.
Along the way, you’ll learn modern best practices, like implementing the Page Object Model (POM), which helps keep your automation scripts clean, testable, and maintainable. Ready to get started? Head over to bandcamp.com/discover/ and play some of the available music to get a feel for the website and pump up your mood for this project!
Get Your Code: Click here to download the free sample code that shows you how to use Selenium in Python for modern web automation.
Take the Quiz: Test your knowledge with our interactive “Web Automation With Python and Selenium” quiz. You’ll receive a score upon completion to help you track your learning progress:
Interactive Quiz
Web Automation With Python and SeleniumIn this quiz, you'll test your understanding of using Selenium with Python for web automation. You'll revisit concepts like launching browsers, interacting with web elements, handling dynamic content, and implementing the Page Object Model (POM) design pattern.
Understand the Project and Approach
Web automation involves using a script to drive a browser and perform actions such as clicking links, filling out forms, and gathering data. Instead of manually navigating a website, you can delegate these tasks to Python. A typical scenario is automating repetitive tasks, such as logging in daily to a tool or scraping regularly updated data.
Because many web apps are built for human interaction, they can present challenges when you try to interact with them automatically. In the early days of the internet, you could send HTTP requests and parse the resulting HTML. But modern sites often rely on JavaScript to handle events or generate content dynamically, meaning that an HTTP request alone probably won’t reveal the full page content. That’s where Selenium comes in.
The Selenium Project
Selenium is a mature open-source project that provides a convenient API to control browsers. With Selenium, you can:
- Launch a headless or visible browser such as Firefox or Chrome using a web driver.
- Visit URLs and navigate pages just like a real user would.
- Locate elements with CSS selectors, XPath, or similar locators.
- Interact with elements by clicking, typing, dragging, or waiting for them to change.
Once you install the appropriate driver for your browser, you can control your browser through a script using Selenium.
Selenium itself is written in Java, but has bindings for different programming languages. In Python, it’s distributed on PyPI as a single package called selenium
, which you can install using pip
.
Selenium is often used for automated testing, but it’s equally useful for generic web automation, which is what this tutorial will focus on.
Note: You might be wondering how Selenium differs from other tools for scripted web interactions, such as Beautiful Soup, Scrapy, or Requests.
One central difference is that those tools are great at handling static data, while Selenium allows you to replicate user behavior at the JavaScript level. This means that you can interact with dynamically generated web content using Selenium.
Before diving into the nuts and bolts of Selenium, it’s helpful to get a clear picture of what you’ll build by the end of this tutorial. As mentioned, you’ll create a fully functional, console-based music player that interacts with the Bandcamp Discover page using a headless Firefox browser.
Your Bandcamp Discover Music Player
Bandcamp is a popular online record store and music community where you can stream songs, explore artists, and discover new albums.
Selenium allows you to automate direct interactions with Bandcamp’s web interface—as though you were clicking and scrolling yourself!
Your finished project will open the Bandcamp Discover page in the background, which means you won’t get to see any of the wonderful album artwork:

If a browser automation tool creates a browser instance without a visible browser window, it’s said to run in headless mode. But don’t lose your head over that word—your code will stay calm and in control!
Read the full article at https://realpython.com/modern-web-automation-with-python-and-selenium/ »
[ Improve Your Python With 🐍 Python Tricks 💌 – Get a short & sweet Python Trick delivered to your inbox every couple of days. >> Click here to learn more and see examples ]
Quiz: Web Automation With Python and Selenium
In this quiz, you’ll test your understanding of Web Automation With Python and Selenium.
By working through this quiz, you’ll revisit concepts like launching browsers, interacting with web elements, handling dynamic content, and implementing the Page Object Model (POM) design pattern.
[ Improve Your Python With 🐍 Python Tricks 💌 – Get a short & sweet Python Trick delivered to your inbox every couple of days. >> Click here to learn more and see examples ]
Anwesha Das
PyCon Lithuania, 2025
Each year, I try to experience a new PyCon. 2025, PyCon Lithuania was added to my PyCon calendar.
Day before the conference
What makes this PyCon, is that we were traveling there as a family and the conference days coincided with the Easter holidays. We utilized that to explore the city—the ancient cathedrals, palaces, old cafes, and of course the Lithuanian cuisine. Šaltibarščiai, Balandeliai and Cepelinai.
Tuesday
22nd, the day before the conference was all about practicing the talk and meeting with the community. We had the pre-conference mingling session with the speakers and volunteers. It was time to meet some old and many new people. Then it was time for PyLadies. Inga from PyLadies Lithuania, Nina from Pyladies London and I had a lovely dinner discussion—good food with the PyLadies community,technology, and us.
Wednesday
The morning started early for us on the day of the conference. All the 3 of us had different responsibilities during the conference. While Py was volunteering, I talked and Kushal was the morning keynoter A Python family in a true sense :)
I had my talk, “Using PyPI Trusted Publishing to Ansible Release” scheduled for the afternoon session. The talk was about automating the Ansible Community package release process with GitHub action using the trusted publisher in PyPI. The talk described - what is trusted publishing.I explanined the need for it and the usage of trusted publishing. I explained the Ansible manual release process in a nutshell and then moved to what the Ansible release process is now with GitHub actions and Trusted Publishing. Then the most important part is, the lessons learned in the process and how other open-source communities can get help and benefit from it.Here is the link for the slides of my talk I had questions regarding trusted publishing, experience as a release manager, and of course Ansible.
It was the time to bid goodbye to PyCon Lt and come back home. See you next year. Congratulatios organizers for doing a great job in organizing the coference.
April 29, 2025
TechBeamers Python
Top Python Code Quality Tools to Improve Your Development Workflow
Are you using tools to improve your Python code quality? If not, you’re missing out! This guide reveals the best code quality tools to write cleaner, faster, and more reliable Python code. Let’s dive in! Writing clean and efficient Python code is crucial for developers. However, maintaining high-quality code can be challenging. Many developers search […]
PyCoder’s Weekly
Issue #679: Regexes, Deep Copy, Enum, and More (April 29, 2025)
#679 – APRIL 29, 2025
View in Browser »
Regex Affordances
A tour of some real code showing little-used power features of the Python regular expression module, including verbose regex syntax, calling re.sub()
with a function reference, and more.
NED BATCHELDER
Shallow vs Deep Copying of Python Objects
What’s the difference between a shallow copy and a deep copy of a Python object? Learn how to clone arbitrary objects in Python, including your own custom classes.
REAL PYTHON
Learn AI In 5 Minutes A Day
Everyone talks about AI, but no one has the time to learn it. So, we found the simplest way to learn AI as quickly as possible: The Rundown AI. It’s the most trusted AI newsletter, with 1M+ readers and exclusives with AI leaders like Mark Zuckerberg, Demis Hassibis, Mustafa Suleyman, and more →
THE RUNDOWN AI sponsor
Module enum
Overview
This article gives an overview of the tools available in the module enum and how to use them, including Enum, auto, StrEnum, Flag, and more.
RODRIGO GIRÃO SERRÃO • Shared by Rodrigo Girão Serrão
Articles & Tutorials
Getting Started With Python IDLE
In this tutorial, you’ll learn how to use the development environment included with your Python installation. Python IDLE is a small program that packs a big punch! You’ll learn how to use Python IDLE to interact with Python directly, work with Python files, and improve your development workflow.
REAL PYTHON
15k Lines of Verified Cryptography Now in Python
Over the last two years there has been an ongoing project to automatically populate the cryptographic libraries in the Python build, helping to ensure their validity and security. This project is now complete, and has been done without effecting the releases along the way.
JONATHAN PROTZENKO
MySQL Databases and Python
In this video course, you’ll learn how to connect your Python application with a MySQL database. You’ll design a movie rating system and perform some common queries on it. You’ll also see best practices and tips to prevent SQL injection attacks.
REAL PYTHON course
Python’s New t-strings
Using f-strings is a readable way of building output, but there are situations where they can’t be used because the contents need to be verified before being string-ified. The new t-strings, coming in 3.14, are a solution to this problem.
DAVE PECK
How to Run Python in Production
This post is a series of recommendations for running Python in a production environment. It includes package management, linters, preventing secrets being leaked, and much more.
ASHISH BHATIA
Feedback Loops in Python
How fast can we get useful feedback on the Python code we write? Learn different techniques to get quick results to better understand if your code is doing what it is supposed to.
DAVIDVUJIC.BLOGSPOT.COM • Shared by David Vujic
Django Admin Theme Roundup 2025
The Django Admin isn’t the prettiest thing out there but there are third party libraries that improve its appearance. This post talks about some of the most popular ones.
ADAM HILL
Choosing the Right Python Task Queue
Python has great options for task queues. Choosing between Celery and RQ isn’t an easy decision. Jump in and learn how each option compares!
JUDOSCALE.COM • Shared by Jeff Morhous
Django Ledger: Accounting With Python
Talk Python interviews Miguel Sanda about Django Ledger, a library you can use to build your own accounting system.
KENNEDY & SANDA
Frankenstein’s __init__
The untold story of the craziest __init__
Ohad has ever seen.
OHAD RAVID
Projects & Code
Events
Weekly Real Python Office Hours Q&A (Virtual)
April 30, 2025
REALPYTHON.COM
PyCamp España 2025
May 1 to May 5, 2025
PYCAMP.ES
Canberra Python Meetup
May 1, 2025
MEETUP.COM
Sydney Python User Group (SyPy)
May 1, 2025
SYPY.ORG
Michigan Python May Meeting
May 1 to May 2, 2025
MEETUP.COM
Happy Pythoning!
This was PyCoder’s Weekly Issue #679.
View in Browser »
[ Subscribe to 🐍 PyCoder’s Weekly 💌 – Get the best Python news, articles, and tutorials delivered to your inbox once a week >> Click here to learn more ]
Real Python
Thread Safety in Python: Locks and Other Techniques
Threads share state in your programs, which means race conditions can be created when two or more threads fight to update a value. This course is about the various primitives you can use to ensure atomic access to your program’s shared state.
By the end of this video course, you’ll be able to identify safety issues and prevent them by using the synchronization primitives in Python’s threading
module to make your code thread-safe.
In this video course, you’ll learn:
- What thread safety is
- What race conditions are and how to avoid them
- How to identify thread safety issues in your code
- What different synchronization primitives exist in the
threading
module - How to use synchronization primitives to make your code thread-safe
[ Improve Your Python With 🐍 Python Tricks 💌 – Get a short & sweet Python Trick delivered to your inbox every couple of days. >> Click here to learn more and see examples ]
PyBites
From Backend to Frontend: Connecting FastAPI and Streamlit
In my previous Pybites article, I showed how I built and deployed a book tracking API using FastAPI, Docker, and Fly.io. That project taught me a lot about backend development, containers, and deploying modern APIs.
But a backend alone isn’t enough—users need an interface. And FastAPI’s Swagger UI, while useful for testing, just isn’t user-friendly enough for everyday use.
So, I decided to build a frontend. My goal? A fast, Python-based UI with minimal hassle. That’s when I turned to Streamlit.
In this article, I’ll explain why I used Streamlit and how I was able to connect my deployed backend to this frontend.
This article will be perfect for those wanting to build a full stack Python application, or even for developers wanting to connect their existing backends to a simple interface.
Why Streamlit?
There are plenty of frontend frameworks out there, but here’s why Streamlit was perfect for my project:
- Speed: I could build and iterate on the interface in minutes, not hours.
- Python-first: Since everything is in Python, I didn’t have to context-switch to HTML/CSS/JS.
- Data-focused: Streamlit is made for data apps, so it has built-in components for tables, charts, and layout.
- Deployable: I could host the frontend on Streamlit Cloud with minimal configuration and connect it to my deployed backend.
Streamlit gave me the option to build a nice looking frontend, without the hassle of going through HTML, CSS, JS, or another tool. I could focus on Python , which is more of my skill set.
Making API Calls from Streamlit
With the backend already being deployed on Fly.io, I just needed to write some client-side logic in the Streamlit app to call it.
I used the requests library to send GET and POST requests. Below is a simplified example of how to connect the Streamlit saved books input to my FastAPI /user-books/
endpoint:
import requests
import streamlit as st
API_URL = "https://your-fastapi-backend.com"
SAVED_BOOKS_URL = f"{API_URL}/user-books/"
st.title("📚 My Saved Books")
# Assume user is already authenticated and token stored in session
access_token = st.session_state.get("access_token")
user_id = st.session_state.get("user_id")
if not access_token or not user_id:
st.error("Please log in to view saved books.")
st.stop()
headers = {"Authorization": f"Bearer {access_token}"}
response = requests.get(SAVED_BOOKS_URL, params={"user_id": user_id}, headers=headers)
if response.status_code != 200:
st.error("Failed to load saved books.")
st.stop()
saved_books = response.json()
for book in saved_books:
st.subheader(book["title"])
st.write(f"**Author(s):** {book['authors']}")
st.write(f"**Published:** {book.get('published_date', 'N/A')}")
st.markdown("---")
This basic example shows how little code is needed to connect and display the data. With that knowledge, it was easy to connect the rest of the FastAPI endpoints to have the app fully functioning.
Handling Environment Variables and CORS
Connecting the frontend and backend isn’t just about sending these API calls. It also requires some behind the scenes work with environment variables and CORS settings to make sure it all runs smoothly.
Environment Variable with a Config Module
Instead of hardcoding environment variables or scattering them around my codebase, I built a dedicated config.py
module using Pydantic Settings.
This lets me load configuration from my .env
file during local development, use os.environ
in Fly.io, and access st.secrets
in Streamlit Cloud.
Here is a simplified example that shows the basics of what I did:
from pydantic_settings import BaseSettings
import streamlit as st
import os
class Settings(BaseSettings):
API_URL: str
# other secrets...
class Config:
env_file = ".env"
def load_settings():
if "database" in st.secrets: # running in Streamlit Cloud
return Settings(API_URL=st.secrets["api"]["API_URL"])
return Settings() # fallback to env vars for local/dev/deploy
settings = load_settings()
This made it easy to access settings across the app with settings.API_URL
, whether running locally, on Fly.io, or on Streamlit Cloud.
I just had to:
- Set up a
.env
file for local use. - Add secrets to Streamlit via the Secrets tab in the cloud dashboard under advanced settings.
- Ensure Fly.io had the same environment variables configured via fly secrets.
CORS Configuration in FastAPI
Since my frontend and backend live on different domains, I ran into a few problems where the browser would block requests. This is where I learned about CORS, or Cross-Origin Resource Sharing.
In my main.py file, I used CORSMiddleware
to explicitly allow requests from the Streamlit frontend. The code looks like this:
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://your-streamlit-app.streamlit.app"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
This simple addition ensured that the frontend could securely access protected API endpoints (like /user-books/
).
Without this, the browser will block requests made from your frontend to a different domain (like your backend API), resulting in errors like CORS policy blocked this request.
Deploying the Streamlit Frontend
Once the frontend was ready, I deployed it using Streamlit Cloud. This is a great service as it’s free and is an easy way to deploy the frontend.
I just followed this simple process:
- Push my code to GitHub
- Link the repo in Streamlit Cloud
- Add my environment variables to the Streamlits Secrets in the app’s setting on Streamlit Cloud
- Click deploy
That was it!
In just a few minutes my full-stack app was live and usable on the web. It was entirely built in Python using FastAPI and Streamlit.
Final Thoughts
This project was a highlight of my Pybites PDM experience.
It showed me how powerful it is to build full-stack apps entirely in Python.
FastAPI gave me a modern, efficient backend, and Streamlit let me ship a clean, interactive frontend in record time.
Another key takeaway was how Docker simplified everything — from local development to deployment on Fly.io.
With containerization, my backend ran the same way everywhere, giving me confidence and consistency.
If you’re a Python dev looking to go full-stack without diving into JavaScript, this approach is perfect.
And if you add Docker into the mix, you’ll be learning modern development workflows that scale well for future projects.
Zato Blog
Sustainable water management with IoT, Open Source and Python
Sustainable water management with IoT, Open Source and Python
"Towards a cyber-physical system for sustainable and smart building: a use case for optimizing water consumption on a SmartCampus" is an interesting, Open Access paper via Springer Publishing authored by Sergio Barroso Ramírez, Pablo Bustos and Pedro Núñez Trujillo of Universidad de Extremadura uses Python, Zato and other open source technologies for IoT integrations.
More resources
➤ Python API integration tutorials
➤ What is an integration platform?
➤ Python Integration platform as a Service (iPaaS)
➤ What is an Enterprise Service Bus (ESB)? What is SOA?
➤ Open-source iPaaS in Pytho
Tryton News
Tryton Release 7.6
We are proud to announce the 7.6 release of Tryton.
This release provides many bug fixes, performance improvements and some fine tuning.
You can give it a try on the demo server, use the docker image or download it here.
As usual upgrading from previous series is fully supported.
Here is a list of the most noticeable changes:
Changes for the User
Client
We added a new menu entry on the list to reset the column widths to their original size.
A new widget has been added on the form to pick a color.
A simple chat widget has been added on the sidebar in beta.
It allows to chat in live between users on specific document.
When saving a CSV export, the option to ignore the search limit is now stored also on the server.
Web
It is now possible to resize the column widths (like on the desktop client).
Accounting
As creating payment term is not always easy, we provide now by default the most common payment terms like “Net 30 days”, “Net 30 days End of Month” etc.
The legacy numbering of account move based on journal has been removed and the Post Number field has been renamed to Number.
Also the numbering of account move is using now a strict sequence to guarantee any missing number.
The sign of the amount of account and analytic budgets has been inverted to be the same as the income statements.
The Spanish AEAT reports are not using a start and end periods instead of a list which may not be continuous.
Depending on the payment method, it is no more always needed to group payments to process them. For example, Stripe and Braintree payments do not create a payment group as they are managed individually.
It is now possible to download all the SEPA message at once.
When entering manually, the statement is using its start date to compute the start balance instead of balance of the last statement. This is useful if for some reasons, you are not entering the statements in the chronological order.
We added a new report displaying each statement journal with its latest date and balance.
It is now possible to order by custom preferences the bank account of a party. This is usefull for defining payment bank account as Tryton always pick the first one by default.
Sale
We manage now the expiration of quotation. A validity duration can be configured for the quotations. It is not possible to confirm a quotation with a sale date after the expiration date. When a quotation reaches its expiration without being confirmed, it will be automatically cancelled.
It is now possible to open a list of all the products from a sale order. This is useful for example to verify the available quantities at once.
A new action has been added to the complaint to create automatically a coupon number from a promotion. It is a common practice to answer to customer complaint by giving them such coupon. This feature prevents to give the access right to create coupon to the user managing the complaints.
The actual quantity of the sale line is now used when processing a complaint instead of the ordered quantity.
We added on the sale the original amount and total before any promotion was applied. This is useful if you want to show those amount to the customer.
We added a menu entry to open all the coupon numbers of promotion. This eases the management when you have a lot of coupon numbers.
Now if a secondary unit is defined on the product customer, then it is filled automatically on the sale line.
Purchase
A date has been added to the purchase orders to store when the quotation will expire. This improves the following of quotations before they expire.
Now if a secondary unit is defined on the product supplier, then it is filled automatically on the purchase line.
Stock
We display the quantity of product when searching for a location; product or lot from a stock move.
The internal shipment gains a packed state when it is between two warehouses. This way we support also to create packages and shipping labels for such shipment.
We also compute the measurements for the internal shipments as needed for some carrier.
The location place is now displayed on inventory line to help finding the products in the location.
Company
It is now possible to define which tax identifier to use for a company per country and/or organization. For example a Belgian company with a Belgian VAT may have a French VAT number when doing business in France.
Country
Tryton can now search subdivision by their shorted code. This provides a better user experience as the full subdivision codes are often prefixed with the country code that the user usually do no type.
Incoterm
We do not require anymore the Incoterm for sale between European countries.
Notification
It is now allowed to set a fallback user on notification without a field set. So the user is always notified.
Party
We support now to enter structured address (with entries for the street name, building number, post box etc), then such address will be formatted following its country. We include in standard the format of 222 countries.
We notify the user if he is about to create a contact mechanism that already exists.
We added new tax identifiers such as the Brazilian Company Identifier, the Canadian Business Number and the Croatian Identification Number.
Web Shop
On Shopify, we archive products that are removed from the shop instead of deleting them. This way if they are added back, we do not loose any customization done in Shopify admin.
In the same way, we do not update the product description if it is empty so they can be fully managed in Shopify admin.
Changes for the System Administrator
Web User
We allow users of the *Party Administrator" group to edit web users.
Changes for the Developer
Server
To follow the official supported Python version, the support of Python 3.13 has been added and support of Python 3.8 has been removed.
We lock now records at the transaction start like for the table.
We include a DecimalNull
in the tools that behaves like the SQL NULL by with Decimal
.
We introduce a chat backend as beta.
Transaction.check_warnings
is now a dictionary used to delete warnings all at once at the end of the transaction.
The target model of a field is now stored in ir.model.field
.
The ModelStorage
has been reworked to add dedicate hooks that replace and simplify in many cases the need to extend the create
, write
and delete
methods. There is now:
preprocess_values
: to prepare the values beforecreate
orwrite
on_modification
: to trigger processes when some records and fields have been modifiedcheck_modification
: to verify if a modification is permittedon_write
: to set methods to be called once the records have been modifiedon_delete
: to set methods to be called once the records have been deleted
And there is now a ModelStorage.compute_fields
method used to store new values for computed fields.
All those changes allowed to remove almost all the extension of the CWD methods reducing the complexity and improve the performance.
The select timeout for the cache channels is now configurable.
The series of trytond
is now stored in the database. This allows the server to prevent to use by mistake a database from a different series.
The wizard state views are now filled with the default values. This removes the need to use getattr
with a default value.
As passlib
does not support Python 3.13, it has been replaced by pwdlib
.
It is now possible to limit the size of the RPC arguments. All the standard RPC methods have been reviewed to add such limitation when it makes sense.
The field_names
argument is now optional when calling Model.default_get
.
The XML data are now always synchronized with the database. So the ModelStorage.check_xml_record
method has been removed.
When searching on Char
and Text
, the None
values are converted as empty string which is the expected behavior by users.
It is now allowed to return instances as default values instead of id. The server will convert them automatically into ids for the client.
The metadata columns are now added automatically to the query of the ModelSQL.table_query
.
The Report.header_key
receive now also the data
on which the report is executed. This permits to group records based on data from a wizard for example.
The integer part of the digits
attribute is now also validated by the server.
We added the support for DATE_TRUNC
and EXTRACT
from INTERVAL
to the SQLite backend.
Tryton now supports up to 4 translations depending on the plural setup of the language.
The methods msg_gettext
and msg_ngettext
are now available in the report context.
Tryton set the Decimal precision of the default context from the TRYTOND_DECIMAL_PREC
environment variable.
We replaced the use of docstring of Model
by a __string__
attribute filled with a default value based on the __name__
value. This allow to run the server with the level 2 of optimization of Python.
The name
field of ir.model
has been renamed into string
and model
into name
and the field_description
of ir.model.field
into string
.
Proteus
We added an helper function launch_action
which allow to launch an action on a lits of records using its XML ID. This ease writing scenario to test those actions.
Client
A new color
widget type has been added for the icon and image fields.
Accounting
A company
argument has been added to method to check the credit limit to be explicit for which company is the amount it checked instead of relying on the contextual value.
The description
field on payment has been renamed into reference
to follow the Tryton naming convention and reflect the actual usage.
The Stripe checkout is now using the Payment Element.
Party
The name
field of address has been rename to building_name
.
The co_rut
tax identifier has been replaced by co_nit
.
Sale
We have added a scheduled task to confirm sales based on payment. This is to strengthen the process in case the payment is succeeded and the sale can not yet be confirmed.
We removed the name
field on the promotion coupon.
2 posts - 1 participant
April 28, 2025
Mike Driscoll
Creating TUI Applications with Textual and Python Kickstarter Launched
Text-based user interfaces (TUIs) are making a BIG comeback. Many developers of today need to easy-to-use applications to configure and run jobs on headless servers. You can make your own life and the lives of your team so much easier by learning how to create a TUI yourself.
Textual is a rapid application development framework for your terminal or web browser that is written in Python. You can build complex, sophisticated applications in your terminal. While terminal applications are text-based rather than pixel-based, they still provide fantastic user interfaces.
Back the Kickstarter Now!
The Textual package allows you to create widgets in your terminal that mimic those used in a web or GUI application.
Creating TUI Applications with Textual and Python is to teach you how to use Textual to make striking applications of your own. The book’s first half will teach you everything you need to know to develop a terminal application.
The book’s second half has many small applications you will learn how to create. Each chapter also includes challenges to complete to help cement what you learn or give you ideas for continued learning.
Here are some of the applications you will create:
- A basic calculator
- A CSV viewer
- A Text Editor
- An MP3 player
- An ID3 Editor
- A Weather application
- A TUI for pre-commit
- RSS Reader
- and more!
Calculator
CSV Viewer
MP3 Player
Weather Application
Text Editor
What You’ll Learn
In this book, you will learn about the following:
- Chapter 1 – Application Basics
- Chapter 2 – Adding Styles in Textual
- Chapter 3 – Using CSS in Textual
- Chapter 4 – Content Markup
- Chapter 5 – Working with DOM Queries
- Chapter 6 – Laying Out Your Widgets
- Chapter 7 – Textual Events and Messages
- Chapter 8 – Key and Mouse Events
- Chapter 9 – Reactive Attributes
- Chapter 10 – Screens
- Chapter 11 – Textual Dev Tools
- Chapter 12 – Creating a Calculator
- Chapter 13 – Viewing Tabular Data with Textual
- Chapter 14 – Creating a Text Editor
- Chapter 15 – Creating an MP3 Player
- Chapter 16 – Creating an ID3 Editor
- Chapter 17 – Creating a Weather App
- Chapter 18 – pre-commit TUI
- Chapter 19 – RSS Reader TUI
- Chapter 20 – SMTP Client
- Chapter 21 – SQLite Viewer/Editor
Rewards to Choose From
As a backer of this Kickstarter, you have some choices to make. You can receive one or more of the following, depending on which level you choose when backing the project:
- An early copy of Creating TUI Applications with Textual and Python + all updates including the final version (ALL BACKERS)
- A signed paperback copy (If you choose the appropriate perk)
- Get all by Python courses (If you choose the appropriate perk)
- Get all TEN of my other Python eBooks (if you choose the appropriate perk)
- T-shirt with the book cover (If you choose the appropriate perk)
Writing Style
This book will be written in my conversational style. Creating TUI Applications with Textual and Python is over 400 pages long and will teach you all you need to know to be able to use the Textual package effectively.
If you’ve never read anything of mine before, you can download the original Python 101 for free or read it online. You can also check out this blog for writing examples.
About the Author
My name is Michael Driscoll, and I am a professional full-time Python programmer by day and Python blogger by night. I have been programming almost exclusively in Python for almost 15 years. I am also a contributor to Real Python. My previous successful campaigns include these other books:
- Python 101 (1st Edition)
- Python 201: Intermediate Python
- ReportLab: PDF Processing with Python
- Jupyter Notebook 101
- Creating GUI Applications with wxPython
- Pillow: Image Processing with Python
- Automating Excel with Python
- and more!
I also have two books that are published by Apress and Packt Publishing:
- wxPython Recipes (Apress)
- Python Interviews (Packt)
Book formats
The finished book will be made available in the following formats:
- paperback (at the appropriate reward level)
- epub
The paperback is a 8.5″ x 11″ book and approximately 450+ pages in length.
Back the Kickstarter Today!
The post Creating TUI Applications with Textual and Python Kickstarter Launched appeared first on Mouse Vs Python.
Real Python
Managing Python Projects With uv: An All-in-One Solution
The uv tool is a high-speed package and project manager for Python. It’s written in Rust and designed to streamline your workflow. It offers fast dependency installation and integrates various functionalities into a single tool.
With uv, you can install and manage multiple Python versions, create virtual environments, efficiently handle project dependencies, reproduce working environments, and even build and publish a project. These capabilities make uv an all-in-one tool for Python project management.
By the end of this tutorial, you’ll understand that:
- uv is a Python package and project manager that integrates multiple functionalities into one tool, offering a comprehensive solution for managing Python projects.
- uv is used for fast dependency installation, virtual environment management, Python version management, and project initialization, enhancing productivity and efficiency.
- uv can build and publish Python packages to package repositories like PyPI, supporting a streamlined process from development to distribution.
- uv automatically handles virtual environments, creating and managing them as needed to ensure clean and isolated project dependencies.
To dive deeper into managing your Python projects efficiently with uv, you should have a basic understanding of using Python virtual environments, setting up pyproject.toml
files for projects, and building distributable packages for a project.
Get Your Code: Click here to download the free sample code you’ll use to learn about managing Python projects with uv.
Take the Quiz: Test your knowledge with our interactive “Managing Python Projects With uv: An All-in-One Solution” quiz. You’ll receive a score upon completion to help you track your learning progress:
Interactive Quiz
Managing Python Projects With uv: An All-in-One SolutionIn this quiz, you'll test your understanding of the uv tool, a high-speed package and project manager for Python.
Getting to Know uv for Python
Recently, a few exciting tools built with the Rust programming language have appeared in the Python tooling ecosystem. Ruff—a linter and code formatter for Python—is a well-known and popular example of one of these tools.
In this tutorial, you’ll explore another cool tool made with Rust for Python. You’ll get to know uv, an extremely fast Python package and project manager.
The main idea behind these tools is to accelerate your Python workflow by speeding up your project management actions. For example, Ruff is 10 to 100 times faster than linters like Flake8 and code formatters like Black. Similarly, for package installation, uv is 10 to 100 times faster than pip
.
Additionally, uv integrates into one tool most of the functionality provided by tools like pip
, pip-tools
, pipx
, poetry
, pyenv
, twine
, virtualenv
, and more. Therefore, uv is an all-in-one solution.
Here’s a quick list of key uv features for managing your Python projects:
- Fast dependency installation: Installs dependencies really fast, which is especially useful for large dependency trees.
- Virtual environment management: Automatically creates and manages virtual environments.
- Python version management: Allows the installation and management of multiple Python versions.
- Project initialization: Scaffolds a full Python project, including the root directory, Git repository, virtual environment,
pyproject.toml
,README
, and more. - Dependency management: Installs, updates, removes, and locks direct and transitive dependencies, which allows for environment reproducibility.
- Package builds and publication management: Allows you to build and publish packages to package repositories like the Python Package Index (PyPI).
- Developer tooling support: Installs and lets you run development tools, such as
pytest
, Black, and Ruff.
Apart from these features, uv is a standalone binary that allows for a smooth installation and quick upgrades. You don’t need to have Python installed on your system to install uv.
So, with this quick summary of uv and its main features, you’re ready to install this tool on your system. That’s what you’ll do in the following section. Additionally, you’ll learn how to update your uv installation.
Installing uv to Manage Python Projects
The first step in using any tool is to install it on your operating system. To install uv, you have several options. The quickest one would be to use the standalone installer. Another friendly option is to install uv from PyPI using other tools like pipx
or pip.
In the official uv installation guide, you’ll find several other installation options. For example, you can use tools like Homebrew and Cargo, depending on your current platform and operating system. However, in this tutorial, you’ll only explore the standalone installer and the PyPI options.
Using the Standalone Installer
The uv project provides a standalone installer that you can use to download and install the tool in your system. Below are the relevant commands for the three main operating systems:
These commands will download and install the latest binary of uv in your system. If you’d like to install a specific version of the tool instead of the latest, then you can add the version number to the download URL right after the uv/
part:
Read the full article at https://realpython.com/python-uv/ »
[ Improve Your Python With 🐍 Python Tricks 💌 – Get a short & sweet Python Trick delivered to your inbox every couple of days. >> Click here to learn more and see examples ]
Quiz: Managing Python Projects With uv: An All-in-One Solution
By working through this quiz, you’ll revisit how Python’s uv integrates multiple functionalities into one tool, offering a comprehensive solution for managing Python projects.
You can use it for fast dependency installation, virtual environment management, Python version management, and project initialization, enhancing your productivity and efficiency.
[ Improve Your Python With 🐍 Python Tricks 💌 – Get a short & sweet Python Trick delivered to your inbox every couple of days. >> Click here to learn more and see examples ]
Talk Python to Me
#503: The PyArrow Revolution
Pandas is at a the core of virtually all data science done in Python, that is virtually all data science. Since it's beginning, Pandas has been based upon numpy. But changes are afoot to update those internals and you can now optionally use PyArrow. PyArrow comes with a ton of benefits including it's columnar format which makes answering analytical questions faster, support for a range of high performance file formats, inter-machine data streaming, faster file IO and more. Reuven Lerner is here to give us the low-down on the PyArrow revolution.<br/> <br/> <strong>Episode sponsors</strong><br/> <br/> <a href='https://talkpython.fm/nordlayer'>NordLayer</a><br> <a href='https://talkpython.fm/auth0'>Auth0</a><br> <a href='https://talkpython.fm/training'>Talk Python Courses</a><br/> <br/> <h2 class="links-heading">Links from the show</h2> <div><strong>Reuven</strong>: <a href="https://github.com/reuven?featured_on=talkpython" target="_blank" >github.com/reuven</a><br/> <strong>Apache Arrow</strong>: <a href="https://github.com/apache/arrow?featured_on=talkpython" target="_blank" >github.com</a><br/> <strong>Parquet</strong>: <a href="https://parquet.apache.org/?featured_on=talkpython" target="_blank" >parquet.apache.org</a><br/> <strong>Feather format</strong>: <a href="https://arrow.apache.org/docs/python/feather.html?featured_on=talkpython" target="_blank" >arrow.apache.org</a><br/> <strong>Python Workout Book (45% off with code talkpython45)</strong>: <a href="https://mng.bz/nZNv?featured_on=talkpython" target="_blank" >manning.com</a><br/> <strong>Pandas Workout Book (45% off with code talkpython45)</strong>: <a href="https://mng.bz/Qwvm?featured_on=talkpython" target="_blank" >manning.com</a><br/> <strong>Pandas</strong>: <a href="https://pandas.pydata.org/?featured_on=talkpython" target="_blank" >pandas.pydata.org</a><br/> <strong>PyArrow CSV docs</strong>: <a href="https://arrow.apache.org/docs/python/csv.html?featured_on=talkpython" target="_blank" >arrow.apache.org</a><br/> <strong>Future string inference in Pandas</strong>: <a href="https://pandas.pydata.org/docs?featured_on=talkpython" target="_blank" >pandas.pydata.org</a><br/> <strong>Pandas NA/nullable dtypes</strong>: <a href="https://pandas.pydata.org/docs/user_guide/integer_na.html?featured_on=talkpython" target="_blank" >pandas.pydata.org</a><br/> <strong>Pandas `.iloc` indexing</strong>: <a href="https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.iloc.html?featured_on=talkpython" target="_blank" >pandas.pydata.org</a><br/> <strong>DuckDB</strong>: <a href="https://duckdb.org?featured_on=talkpython" target="_blank" >duckdb.org</a><br/> <strong>Pandas user guide</strong>: <a href="https://pandas.pydata.org/docs/user_guide/?featured_on=talkpython" target="_blank" >pandas.pydata.org</a><br/> <strong>Pandas GitHub issues</strong>: <a href="https://github.com/pandas-dev/pandas/issues?featured_on=talkpython" target="_blank" >github.com</a><br/> <strong>Watch this episode on YouTube</strong>: <a href="https://www.youtube.com/watch?v=IHd-bgeHrv0" target="_blank" >youtube.com</a><br/> <strong>Episode transcripts</strong>: <a href="https://talkpython.fm/episodes/transcript/503/the-pyarrow-revolution" target="_blank" >talkpython.fm</a><br/> <br/> <strong>--- Stay in touch with us ---</strong><br/> <strong>Subscribe to Talk Python on YouTube</strong>: <a href="https://talkpython.fm/youtube" target="_blank" >youtube.com</a><br/> <strong>Talk Python on Bluesky</strong>: <a href="https://bsky.app/profile/talkpython.fm" target="_blank" >@talkpython.fm at bsky.app</a><br/> <strong>Talk Python on Mastodon</strong>: <a href="https://fosstodon.org/web/@talkpython" target="_blank" ><i class="fa-brands fa-mastodon"></i>talkpython</a><br/> <strong>Michael on Bluesky</strong>: <a href="https://bsky.app/profile/mkennedy.codes?featured_on=talkpython" target="_blank" >@mkennedy.codes at bsky.app</a><br/> <strong>Michael on Mastodon</strong>: <a href="https://fosstodon.org/web/@mkennedy" target="_blank" ><i class="fa-brands fa-mastodon"></i>mkennedy</a><br/></div>
Python Bytes
#430 Or you go to jail
<strong>Topics covered in this episode:</strong><br> <ul> <li><strong><a href="https://ichard26.github.io/blog/2025/04/whats-new-in-pip-25.1/?featured_on=pythonbytes">pip 25.1 has dependency groups, pylock.toml, plus more</a></strong></li> <li><strong><a href="https://bsky.app/profile/aiohttp.org/post/3lmyhz6uhks2u?featured_on=pythonbytes">aiohttp goes free threaded</a></strong></li> <li><strong><a href="https://github.com/astral-sh/uv/releases/tag/0.6.15?featured_on=pythonbytes">uv 0.6.15 supports pylock.toml</a></strong></li> <li><a href="https://github.com/ariebovenberg/whenever?featured_on=pythonbytes"><strong>Whenever</strong></a></li> <li><strong>Extras</strong></li> <li><strong>Joke</strong></li> </ul><a href='https://www.youtube.com/watch?v=BGhDge-iUTw' style='font-weight: bold;'data-umami-event="Livestream-Past" data-umami-event-episode="430">Watch on YouTube</a><br> <p><strong>About the show</strong></p> <p>Sponsored by <a href="https://pythonbytes.fm/porkbun"><strong>Porkbun</strong></a>! Use our link <a href="https://pythonbytes.fm/porkbun"><strong>pythonbytes.fm/porkbun</strong></a> and get a .app or .dev domain for $5.99 at Porkbun.</p> <p><strong>Connect with the hosts</strong></p> <ul> <li>Michael: <a href="https://fosstodon.org/@mkennedy"><strong>@mkennedy@fosstodon.org</strong></a> <strong>/</strong> <a href="https://bsky.app/profile/mkennedy.codes?featured_on=pythonbytes"><strong>@mkennedy.codes</strong></a> <strong>(bsky)</strong></li> <li>Brian: <a href="https://fosstodon.org/@brianokken"><strong>@brianokken@fosstodon.org</strong></a> <strong>/</strong> <a href="https://bsky.app/profile/brianokken.bsky.social?featured_on=pythonbytes"><strong>@brianokken.bsky.social</strong></a></li> <li>Show: <a href="https://fosstodon.org/@pythonbytes"><strong>@pythonbytes@fosstodon.org</strong></a> <strong>/</strong> <a href="https://bsky.app/profile/pythonbytes.fm"><strong>@pythonbytes.fm</strong></a> <strong>(bsky)</strong></li> </ul> <p>Join us on YouTube at <a href="https://pythonbytes.fm/stream/live"><strong>pythonbytes.fm/live</strong></a> to be part of the audience. Usually <strong>Monday</strong> at 10am PT. Older video versions available there too.</p> <p>Finally, if you want an artisanal, hand-crafted digest of every week of the show notes in email form? Add your name and email to <a href="https://pythonbytes.fm/friends-of-the-show">our friends of the show list</a>, we'll never share it. </p> <p><strong>Brian #1:</strong> <a href="https://ichard26.github.io/blog/2025/04/whats-new-in-pip-25.1/?featured_on=pythonbytes">pip 25.1 has dependency groups, pylock.toml, plus more</a></p> <ul> <li>post <a href="https://ichard26.github.io/blog/2025/04/whats-new-in-pip-25.1/?utm_source=pocket_shared&featured_on=pythonbytes">What's new in pip 25.1 - Dependency groups!</a></li> <li>Richard Si</li> <li>Discovered this through <a href="https://bsky.app/profile/hugovk.dev/post/3lnqd2fosq224?featured_on=pythonbytes">Hugo van Kemenade</a></li> <li><p>Dependency groups, PEP 735, supported</p> <pre><code># pyproject.toml [dependency-groups] test = ["pytest", "pytest-xdist"] lint = ["mypy", "isort"] # Dependency Groups can include other groups! ✨ dev = [ {include-group = "test"}, {include-group = "lint"} ] </code></pre></li> <li><p>Package installation progress bar</p></li> <li>Resumable downloads</li> <li>Experimental lockfile generation, PEP 751, with pip lock <ul> <li>so cool</li> </ul></li> <li>pip index versions is stable, no longer experimental <ul> <li>use this to get a list of available versions</li> <li>ex: python3 -m pip index versions pytest-check</li> <li>combine with --json to get a nice script readable output</li> </ul></li> </ul> <p><strong>Michael #2:</strong> <a href="https://bsky.app/profile/aiohttp.org/post/3lmyhz6uhks2u?featured_on=pythonbytes">aiohttp goes free threaded</a></p> <ul> <li>Thanks to months of consistent contributions by Lysandros Nikolaou, all of the mandatory dependencies of <a href="https://www.dropbox.com/?q=%23aiohttp&featured_on=pythonbytes">#aiohttp</a> now ship free-threaded variants of <a href="https://www.dropbox.com/?q=%23wheels&featured_on=pythonbytes">#wheels</a>!</li> <li>This unlocks the same in <a href="https://github.com/aio-libs/aiohttp?featured_on=pythonbytes">aiohttp</a>!</li> </ul> <p><strong>Brian #3:</strong> <a href="https://github.com/astral-sh/uv/releases/tag/0.6.15?featured_on=pythonbytes">uv 0.6.15 supports pylock.toml</a></p> <ul> <li>Discovered through <a href="https://bsky.app/profile/snarky.ca/post/3lngwrbkbm22g?featured_on=pythonbytes">Brett Cannon</a></li> <li>So far, these projects support pylock.toml <ul> <li>pip</li> <li>pip-audit</li> <li>pdm</li> <li>uv</li> </ul></li> <li>With uv <ul> <li>To export a uv.lock to the pylock.toml format, <ul> <li>run: uv export -o pylock.toml</li> </ul></li> <li>To generate a pylock.toml file from a set of requirements, <ul> <li>run: uv pip compile -o pylock.toml -r requirements.in</li> </ul></li> <li>To install from a pylock.toml file, <ul> <li>run: uv pip sync pylock.toml or uv pip install -r pylock.toml</li> </ul></li> </ul></li> </ul> <p><strong>Michael #4:</strong> <a href="https://github.com/ariebovenberg/whenever?featured_on=pythonbytes"><strong>Whenever</strong></a></p> <ul> <li>via Pat Decker</li> <li>Typed and DST-safe datetimes for Python, available in Rust or pure Python.</li> <li><em>Whenever</em> helps you write <strong>correct</strong> and <strong>type checked</strong> datetime code.</li> <li>It's also <strong>way faster</strong> than other third-party libraries—and usually the standard library as well.</li> </ul> <p><strong>Extras</strong> </p> <p>Brian:</p> <ul> <li><a href="https://everyuuid.com?featured_on=pythonbytes">Every UUID</a> </li> </ul> <p>Michael:</p> <ul> <li><a href="https://www.pillar.security/blog/new-vulnerability-in-github-copilot-and-cursor-how-hackers-can-weaponize-code-agents?featured_on=pythonbytes">New Vulnerability in GitHub Copilot and Cursor: How Hackers Can Weaponize Code Agents</a> via Brian Skinn</li> <li>And <a href="https://www.darkreading.com/application-security/ai-code-tools-widely-hallucinate-packages?featured_on=pythonbytes">typosquatting in the AI age</a></li> <li>Firefox Send alternatives <ul> <li><a href="https://github.com/kern/filepizza?featured_on=pythonbytes">file.pizza</a> via <a href="https://social.tchncs.de/@rafaelwo/114393487740735715?featured_on=pythonbytes">@rafaelwo</a> </li> <li><a href="https://bitwarden.com/products/send/?featured_on=pythonbytes">bitwarden send</a></li> </ul></li> </ul> <p><strong>Joke:</strong> <strong>Can you Vibe?</strong></p> <ul> <li><a href="https://www.youtube.com/watch?v=JeNS1ZNHQs8"><strong>Interview with Vibe Coder in 2025</strong></a></li> <li><a href="https://www.youtube.com/watch?v=_2C2CNmK7dQ"><strong>Senior Engineer tries Vibe Coding</strong></a></li> </ul>
Python GUIs
Kivy's Complex Widgets — Learn How to Use Kivy's Complex UX Widgets in Your Apps
Kivy is a powerful framework for developing multi-touch GUI applications using Python. It provides a set of rich built-in widgets which you can use to build complex GUI applications.
In a previous tutorial we covered the basic Kivy widgets such as text inputs, buttons and checkboxes. In this tutorial, we will take things further, exploring some more of the more complex widgets that Kivy provides. These include: Bubble
, DropDown
, FileChooser
, Popup
, Spinner
, RecycleView
, TabbedPanel
, VideoPlayer
, and VKeyboard
. With them, you can add advanced features to your Kivy apps.
- Writing an Outline Kivy App
- Providing Option Selections With Spinner
- Providing Options With DropDown List
- Accessing Files With FileChooser*
- Building Quick Dialogs With Popup
- Creating Contextual Popups With Bubble
- Displaying Data With RecycleView
- Building Tabbed UIs With TabbedPanel
- Allowing User Input With VKeyboard
- Conclusion
Writing an Outline Kivy App
We'll start this tutorial with a simple application skeleton, which we will then modify below. Save the following code in a file named app.py
:
from kivy.app import App
from kivy.core.window import Window
from kivy.uix.boxlayout import BoxLayout
class WidgetNameApp(App):
title = "WidgetName Widget"
def build(self):
Window.clearcolor = (0, 0.31, 0.31, 1.0)
Window.size = (360, 640)
root = BoxLayout()
return root
WidgetNameApp().run()
Here, we've created a Kivy application with an empty window. The BoxLayout
acts as the root widget, this will act as the container to add our complex widgets to. The build()
method sets the window's background color to a dark teal shade and adjusts the window size to 360x640 pixels, which is a mobile-friendly size.
To learn more about creating your first Kivy app, check out the Getting Started With Kivy for GUI Development tutorial.
Providing Option Selections With Spinner
The Spinner
is a dropdown selector that allows users to choose one option from multiple choices. This is ideal when working with a list of simple text choices. Below is an example that builds a Spinner
that lets you select from different parts of this website.
from kivy.app import App
from kivy.core.window import Window
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.spinner import Spinner
class SpinnerApp(App):
title = "Spinner Widget"
def build(self):
Window.clearcolor = (0, 0.31, 0.31, 1.0)
Window.size = (300, 300)
root = FloatLayout()
# Create the Spinner
spinner = Spinner(
text="Home",
values=("Home", "Latest", "FAQ", "Forum", "Contact", "About"),
size_hint=(None, None),
size=(200, 70),
pos_hint={"center_x": 0.2, "center_y": 0.9},
sync_height=True,
)
root.add_widget(spinner)
return root
SpinnerApp().run()
The Spinner
widget works as a simple dropdown list, allowing users to select one option from multiple text choices. We've set the Spinner
to start with "Home"
as the default text
and provided other options ("Latest"
, "FAQ"
, "Forum"
, "Contact"
, and "About"
) as a list of values.
You need to repeat the "Home"
option in values
so that you don't lose it when you select another option.
Run it! You'll get an app that looks as shown below.
A Kivy app showing a
Spinner
Widget
The dropdown spinner allows users to select from predefined choices. You can use this widget to create elements that work like dropdown list, optimizing space and providing a clean UI.
Providing Options With DropDown
List
The DropDown
widget provides a more complex menu component that allows users to choose from multiple options. Like spinner, this provides an intuitive way for users to select from a set of choices, but here you can display more than just text. This makes it more complex to use, but allows for more flexibility.
Below is an example of using the widget to create a dropdown list that lets you select your favorite GUI library, displayed on a series of Button
objects.
from kivy.app import App
from kivy.core.window import Window
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.dropdown import DropDown
class DropDownApp(App):
title = "DropDown Widget"
def build(self):
Window.clearcolor = (0, 0.31, 0.31, 1.0)
Window.size = (200, 200)
root = BoxLayout(orientation="vertical", padding=10, spacing=10)
# Create a dropdown with 4 buttons
dropdown = DropDown()
for item in ["Kivy", "PyQt6", "PySide6", "Tkinter"]:
option_btn = Button(text=item, size_hint_y=None, height=50, width=150)
option_btn.bind(on_release=lambda btn: dropdown.select(btn.text))
dropdown.add_widget(option_btn)
# Create a main button to show the dropdown
button = Button(
text="Library",
size_hint=(None, None),
size=(150, 50),
)
button.bind(on_release=dropdown.open)
dropdown.bind(
on_select=lambda instance, text: setattr(button, "text", text),
)
root.add_widget(button)
return root
DropDownApp().run()
In this example, we have a DropDown
widget that lets the user to select a library from a list of options. You populate the dropdown with four options "Kivy"
, "PyQt6"
, "PySide6"
, and "Tkinter"
, which are displayed in Button
objects.
We set each button to trigger the dropdown.select()
method when clicked, passing the button's text as the selected value.
Then, we anchor the dropdown to a Library button. When we press the Library button, the dropdown menu opens, displaying the options. Once we select an option, the on_select
event updates the main button's text to reflect the chosen library.
Run it! You'll get a window with a dropdown list in the lower left corner. Click in the dropdown widget to change the current selection.
A Kivy app showing a
DropDown
widget
Accessing Files With FileChooser*
The filechooser
module provides classes for describing, displaying and browsing file systems. In this module there are two ready-made widget views which present the file system as either a list, or as icons. The example below demonstrates these both in action.
from kivy.app import App
from kivy.core.window import Window
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.filechooser import FileChooserIconView, FileChooserListView
class FileChooserApp(App):
title = "FileChooser Widget"
def build(self):
Window.clearcolor = (0, 0.31, 0.31, 1.0)
Window.size = (360, 640)
root = BoxLayout(orientation="vertical")
# Create icon-view and list-view file choosers
filechooser_icons = FileChooserIconView()
filechooser_list = FileChooserListView()
root.add_widget(filechooser_icons)
root.add_widget(filechooser_list)
return root
FileChooserApp().run()
In this example, we create file chooser widgets to browse and select files using two different views:
- Icon view (
FileChooserIconView
) - List view (
FileChooserListView
)
The icon view displays files as wrapped rows of icons, clicking on a folder icon will navigate down into that folder. The list view presents them in a list-tree like format, where clicking on a folder will show files and folders nested under it.
Run it! On macOS, you'll see a window that looks something like the following. Try clicking on the folder icons and entries in the list view to see how navigation works in the two examples.
A Kivy app showing file chooser widgets
Building Quick Dialogs With Popup
The Popup
widget allows us to display modal dialogs with custom content, layouts and widgets. They can be used to show messages or ask for input. The following popup message displays a simple message, with a title, message and OK button.
from kivy.app import App
from kivy.core.window import Window
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.label import Label
from kivy.uix.popup import Popup
class PopupApp(App):
title = "Popup Widget"
def build(self):
Window.clearcolor = (0, 0.31, 0.31, 1.0)
Window.size = (400, 400)
root = FloatLayout()
button = Button(
text="Open Popup",
on_press=lambda x: self.show_popup(),
size_hint=(None, None),
size=(200, 50),
pos_hint={"center_x": 0.5, "center_y": 0.5},
)
root.add_widget(button)
return root
def show_popup(self):
# Create and show the Popup
popup = Popup(
title="Info",
size_hint=(0.6, 0.6),
size=(300, 300),
auto_dismiss=False,
)
layout = BoxLayout(orientation="vertical", spacing=10, padding=10)
message = Label(text="Hello, World!")
ok_button = Button(text="OK", size_hint=(None, None), size=(80, 40))
ok_button.bind(on_release=popup.dismiss)
layout.add_widget(message)
layout.add_widget(ok_button)
popup.content = layout
popup.open()
PopupApp().run()
In this example, you create a Popup
widget that displays information as a modal dialog. When the user clicks the Open Popup button, the show_popup()
method is triggered, creating a Popup
that occupies 60% of the screen in both directions.
We set auto_dismiss
to False
, which means the popup won't close if we click outside of it. The popup contains the Hello, World!
message and an OK button. When we click the button, we dismiss (close) the popup. Popups are effective for displaying alerts, confirmations, or other information in a Kivy app.
Run it! You'll get a window with a button labeled "Open Popup". Click on the Open Popup button to display the popup window.
A Kivy app showing a popup dialog
Creating Contextual Popups With Bubble
The Bubble
widget is a UI element commonly used for contextual popups, tooltips, or chat applications. Below is a quick Kivy application that shows some text and lets you click on it to change its format. We'll start by importing the necessary objects and subclassing the Bubble
class:
from kivy.app import App
from kivy.core.window import Window
from kivy.metrics import dp
from kivy.uix.bubble import Bubble, BubbleButton, BubbleContent
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.label import Label
class FormattingBubble(Bubble):
def __init__(self, target_text, **kwargs):
super().__init__(**kwargs)
# Customizing the bubble
self.size_hint = (None, None)
self.size = (dp(120), dp(50))
self.arrow_pos = "top_mid"
self.orientation = "horizontal"
self.target_label = target_text
# Add formatting buttons
bold_btn = BubbleButton(text="Bold")
italic_btn = BubbleButton(text="Italic")
bold_btn.bind(on_release=lambda x: self.on_format("bold"))
italic_btn.bind(on_release=lambda x: self.on_format("italic"))
# Add the buttons to the bubble
bubble_content = BubbleContent()
bubble_content.add_widget(bold_btn)
bubble_content.add_widget(italic_btn)
self.add_widget(bubble_content)
def on_format(self, format_type):
if format_type == "bold":
self.target_label.text = f"[b]{self.target_label.text}[/b]"
elif format_type == "italic":
self.target_label.text = f"[i]{self.target_label.text}[/i]"
self.parent.remove_widget(self)
class BubbleApp(App):
title = "Bubble Widget"
def build(self):
Window.clearcolor = (0, 0.31, 0.31, 1.0)
Window.size = (360, 640)
root = FloatLayout()
self.text = Label(
text="Click this text to apply formatting",
size_hint=(0.8, 0.2),
pos_hint={"center_x": 0.5, "center_y": 0.5},
markup=True,
)
self.text.bind(on_touch_down=self.show_bubble)
root.add_widget(self.text)
root.bind(on_touch_down=self.dismiss_bubbles)
return root
def show_bubble(self, instance, touch):
if instance.collide_point(*touch.pos):
self.remove_all_bubbles()
bubble = FormattingBubble(target_text=self.text)
bubble.pos = (
touch.x - bubble.width / 2, touch.y - bubble.height - dp(10)
)
self.root.add_widget(bubble)
def dismiss_bubbles(self, instance, touch):
if instance == self.root and not self.text.collide_point(*touch.pos):
self.remove_all_bubbles()
def remove_all_bubbles(self):
for widget in self.root.children[:]:
if isinstance(widget, FormattingBubble):
self.root.remove_widget(widget)
return
BubbleApp().run()
The FormattingBubble
class inherits from Bubble
and provides text formatting options for a label. It initializes with a specific size, arrow position, and horizontal layout. The bubble will contain two buttons: Bold and Italic. When pressed, these buttons apply the respective formatting to the target text by triggering the on_format()
method. This method wraps the text in Kivy's markup tags [b]...[/b]
for bold and [i]...[/i]
for italic.
The BubbleApp
class represents the Kivy application. It sets up a FloatLayout
with a centered Label
displaying a message. When the user taps the label, the show_bubble()
method creates and positions a FormattingBubble
above the tapped location.
The app also ensures that only one bubble is visible at a time by removing existing ones before showing a new one. Additionally, tapping outside the label dismisses any active bubbles using the dismiss_bubbles()
method.
Run it! The app features a dark teal background and a mobile-friendly window size. The Bubble
widget appears when we click the text.
A Kivy app showing a
Bubble
widget
Displaying Data With RecycleView
The RecycleView
widget efficiently displays data by recycling views or graphical elements. Instead of creating a widget for every item in the dataset, RecycleView
reuses a small number of widgets to display visible items only, improving performance.
To illustrate, let's create a view that lets you inspect a database of employee profiles. The data is stored in a CSV file that looks like the following:
name,job,department
John Smith,Developer,IT
Jane Doe,Designer,Graphics
Anne Frank,Artist,Painting
David Lee,Engineer,Civil
Ella Brown,Doctor,Medical
Frank Thomas,Chef,Culinary
Henry Ford,Driver,Transport
Nathan Young,Consultant,Business
Olivia King,Manager,Administration
Peter Wright,Director,Management
Queen Bell,President,Executive
Walter Thompson,Assistant,Support
Xena Garcia,Associate,Associate
Zack Harris,Consultant,Consulting
You can read and load this data with the csv
module. To visualize the data, you can create a view with the RecycleView
widget. For the individual views, you can use the Button
widget, which will let you display the employee's profile:
import csv
from kivy.app import App
from kivy.core.window import Window
from kivy.metrics import dp
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.uix.popup import Popup
from kivy.uix.recycleboxlayout import RecycleBoxLayout
from kivy.uix.recycleview import RecycleView
class EmployeesView(RecycleView):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.employees_data = self._read_from_csv()
# Load the employees data into the data attribute
self.data = [
{
"text": f"{employee['name']}",
"on_release": self._create_callback(employee["name"]),
}
for employee in self.employees_data
]
layout_manager = RecycleBoxLayout(
default_size=(None, dp(56)),
default_size_hint=(1, None),
size_hint_y=None,
orientation="vertical",
)
layout_manager.bind(minimum_height=layout_manager.setter("height"))
self.add_widget(layout_manager)
self.viewclass = "Button"
def _create_callback(self, name):
return lambda: self.on_button_click(name)
def _read_from_csv(self):
with open("employees.csv", mode="r") as file:
return [row for row in csv.DictReader(file)]
def on_button_click(self, name):
popup = Popup(
title=f"{name}'s Profile",
size_hint=(0.8, 0.5),
size=(300, 300),
auto_dismiss=False,
)
employees_data = [
employee for employee in self.employees_data if employee["name"] == name
]
profile = "\n".join(
[f"{key.capitalize()}: {value}" for key, value in employees_data[0].items()]
)
layout = BoxLayout(orientation="vertical", spacing=10, padding=10)
message = Label(text=profile)
ok_button = Button(text="OK", size_hint=(None, None))
ok_button.bind(on_release=popup.dismiss)
layout.add_widget(message)
layout.add_widget(ok_button)
popup.content = layout
popup.open()
class RecycleViewApp(App):
title = "RecycleView Widget"
def build(self):
Window.clearcolor = (0, 0.31, 0.31, 1.0)
Window.size = (360, 640)
return EmployeesView()
RecycleViewApp().run()
In this example, we subclass RecycleView
to display the list of employees loaded from a CSV file. The _read_from_csv()
method opens the file and reads the data using the csv.DictReader()
class, which converts each CSV line into a dictionary whose keys come from the file header line.
The data
attribute is key for the app to work because it'll hold the data that we want to display. To arrange widgets in a RecycleView
, we use a RecycleBoxLayout
. The viewclass
attribute lets us set the widget that we'll use to display each data item.
It's important to note that for the RecycleView
to work properly, we should set viewclass
at the end when the data is already loaded and the layout is set up.
Then, we populate the RecycleView
view with buttons, each displaying an employee's name. Clicking a button triggers _create_callback()
, which generates a callback that opens a popup displaying the selected employee's profile details.
Run it! You'll get a nice-looking window listing the employees. Click a button to view the associated employee's profile. Scroll down to load more profiles.
A Kivy app showing a
RecycleView
Widget
Building Tabbed UIs With TabbedPanel
The TabbedPanel
widget lets us organize content into tabs, to improve navigation and optimize the use of space. This is commonly used in settings dialogs where there are lots of options available.
from kivy.app import App
from kivy.core.window import Window
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelHeader
class TabbedPanelApp(App):
title = "TabbedPanel Widget"
def build(self):
Window.clearcolor = (0, 0.31, 0.31, 1.0)
Window.size = (360, 640)
# Create the TabbedPanel
root = TabbedPanel(do_default_tab=False)
# Create the tabs
general_tab = TabbedPanelHeader(text="General")
general_content = BoxLayout(orientation="vertical", padding=10, spacing=10)
general_content.add_widget(Label(text="General Settings", font_size=40))
general_tab.content = general_content
root.add_widget(general_tab)
editor_tab = TabbedPanelHeader(text="Editor")
editor_content = BoxLayout(orientation="vertical", padding=10, spacing=10)
editor_content.add_widget(Label(text="Editor Settings", font_size=40))
editor_tab.content = editor_content
root.add_widget(editor_tab)
profile_tab = TabbedPanelHeader(text="Profile")
profile_content = BoxLayout(orientation="vertical", padding=10, spacing=10)
profile_content.add_widget(Label(text="User Profile", font_size=40))
profile_tab.content = profile_content
root.add_widget(profile_tab)
return root
TabbedPanelApp().run()
In this example, we create a Kivy app that shows a tabbed interface using the TabbedPanel
widget. It disables the default tab and manually adds three tabs: General, Editor, and Profile, each represented by a TabbedPanelHeader
object.
Inside the tabs, we place a BoxLayout
to hold a label that displays a description as a placeholder tab content. Tabs allow us to organize content into visually distinct sections within an application's UI.
Run it! Your app will display three tabs. When you click the tab header, the app shows the tab's content. The active tab shows a light blue line at the bottom.
A Kivy app showing a
TabbedPanel
Widget
Try and add some more widgets to each tab panel.
Allowing User Input With VKeyboard
The VKeyboard
widget allows you to create a virtual keyboard that is useful for touchscreen applications that require the user to type in text. Below is a short app that demonstrates a virtual keyboard in action. When you type text using the keyboard, it is displayed on the label.
from kivy.app import App
from kivy.core.window import Window
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.vkeyboard import VKeyboard
class VKeyboardApp(App):
title = "VKeyboard Widget"
def build(self):
Window.clearcolor = (0, 0.31, 0.31, 1.0)
Window.size = (360, 640)
root = BoxLayout(orientation="vertical")
self.display_label = Label(text="Type in!", font_size=40)
root.add_widget(self.display_label)
# Create the virtual keyboard
keyboard = VKeyboard(size_hint=(1, 0.4))
keyboard.bind(on_key_up=self.keyboard_on_key_up)
root.add_widget(keyboard)
return root
def keyboard_on_key_up(self, *args):
keycode = args[1]
text = args[2]
if keycode == "backspace":
if (
len(self.display_label.text) > 0
and self.display_label.text != "Type in!"
):
self.display_label.text = self.display_label.text[:-1]
if self.display_label.text == "":
self.display_label.text = "Type in!"
elif keycode == "spacebar":
if self.display_label.text == "Type in!":
self.display_label.text = " "
else:
self.display_label.text += " "
elif keycode in {"enter", "shift", "alt", "ctrl", "escape", "tab", "capslock"}:
pass
else:
if self.display_label.text == "Type in!":
self.display_label.text = text
else:
self.display_label.text += text
VKeyboardApp().run()
In this example, we manually add a virtual keyboard to our app's interface using the VKeyboard
widget and display typed text using a label.
When a key is released, the keyboard_on_key_up()
method processes the input. Printable characters are appended to the label text. Backspace removes the last character, and the spacebar inserts a space.
You typically wouldn't use the VKeyboard
widget as in the example above. Input widgets, like TextInput
, will automatically bring up the virtual keyboard when focused on mobile devices.
We ignore special keys like Enter, Shift, Alt, Ctrl, and Escape. This allows us to interact with a virtual keyboard and see the input displayed dynamically in the label.
Run it! A virtual keyboard appears at the button of the app's window, allowing you to enter text in touch-based devices. When you type on the virtual keyboard at the bottom of the app's window, the label reflects what you've typed.
A Kivy app showing a
VKeyboard
Widget
Conclusion
Kivy provides a rich set of complex UX widgets that you can use to create cross-platform applications. Using the examples above as inspiration you should now be able to use Bubble
, DropDown
, FileChooser
, Popup
, Spinner
, RecycleView
, TabbedPanel
, and VKeyboard
in your own apps. See if you can extend these examples further, adding more widgets or functionality to them.
April 27, 2025
Real Python
Quiz: How to Manage Python Projects With pyproject.toml
In this quiz, you’ll test your understanding of the pyproject.toml
file.
This file is a key component for defining a Python project’s build system, including its requirements and build backend. With appropriate tooling, it can also manage dependencies, optional dependencies, command-line scripts, and dynamic metadata for flexible project configuration.
[ Improve Your Python With 🐍 Python Tricks 💌 – Get a short & sweet Python Trick delivered to your inbox every couple of days. >> Click here to learn more and see examples ]