Planet Python
Last update: December 27, 2025 09:44 PM UTC
December 26, 2025
"Michael Kennedy's Thoughts on Technology"
DevOps Python Supply Chain Security

In my last article, “Python Supply Chain Security Made Easy” I talked about how to automate pip-audit so you don’t accidentally ship malicious Python packages to production. While there was defense in depth with uv’s delayed installs, there wasn’t much safety beyond that for developers themselves on their machines.
This follow up fixes that so even dev machines stay safe.
Defending your dev machine
My recommendation is instead of installing directly into a local virtual environment and then running pip-audit, create a dedicated Docker image meant for testing dependencies with pip-audit in isolation.
Our workflow can go like this.
First, we update your local dependencies file:
uv pip compile requirements.piptools --output-file requirements.txt --exclude-newer 1 week
This will update the requirements.txt file, or tweak the command to update your uv.lock file, but it don’t install anything.
Second, run a command that uses this new requirements file inside of a temporary docker container to install the requirements and run pip-audit on them.
Third, only if that pip-audit test succeeds, install the updated requirements into your local venv.
uv pip install -r requirements.txt
The pip-audit docker image
What do we use for our Docker testing image? There are of course a million ways to do this. Here’s one optimized for building Python packages that deeply leverages uv’s and pip-audit’s caching to make subsequent runs much, much faster.
Create a Dockerfile with this content:
# Image for installing python packages with uv and testing with pip-audit
# Saved as Dockerfile
FROM ubuntu:latest
RUN apt-get update
RUN apt-get upgrade -y
RUN apt-get autoremove -y
RUN apt-get -y install curl
# Dependencies for building Python packages
RUN apt-get install -y gcc
RUN apt-get install -y build-essential
RUN apt-get install -y clang
RUN apt-get install -y openssl
RUN apt-get install -y checkinstall
RUN apt-get install -y libgdbm-dev
RUN apt-get install -y libc6-dev
RUN apt-get install -y libtool
RUN apt-get install -y zlib1g-dev
RUN apt-get install -y libffi-dev
RUN apt-get install -y libxslt1-dev
ENV PATH=/venv/bin:$PATH
ENV PATH=/root/.cargo/bin:$PATH
ENV PATH=/root/.local/bin/:$PATH
ENV UV_LINK_MODE=copy
# Install uv
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
# set up a virtual env to use for temp dependencies in isolation.
RUN --mount=type=cache,target=/root/.cache uv venv --python 3.14 /venv
# test that uv is working
RUN uv --version
WORKDIR "/"
# Install pip-audit
RUN --mount=type=cache,target=/root/.cache uv pip install --python /venv/bin/python3 pip-audit
This installs a bunch of Linux libraries used for edge-case builds of Python packages. It takes a moment, but you only need to build the image once. Then you’ll run it again and again. If you want to use a newer version of Python later, change the version in uv venv --python 3.14 /venv. Even then on rebuilds, the apt-get steps are reused from cache.
Next you build with a fixed tag so you can create aliases to run using this image:
# In the same folder as the Dockerfile above.
docker build -t pipauditdocker .
Finally, we need to run the container with a few bells and whistles. Add caching via a volume so subsequent runs are very fast: -v pip-audit-cache:/root/.cache. And map a volume so whatever working directory you are in will find the local requirements.txt: -v \"\$(pwd)/requirements.txt:/workspace/requirements.txt:ro\"
Here is the alias to add to your .bashrc or .zshrc accomplishing this:
alias pip-audit-proj="echo 'đł Launching isolated test env in Docker...' && \
docker run --rm \
-v pip-audit-cache:/root/.cache \
-v \"\$(pwd)/requirements.txt:/workspace/requirements.txt:ro\" \
pipauditdocker \
/bin/bash -c \"echo 'đŠ Installing requirements from /workspace/requirements.txt...' && \
uv pip install --quiet -r /workspace/requirements.txt && \
echo 'đ Running pip-audit security scan...' && \
/venv/bin/pip-audit \
--ignore-vuln CVE-2025-53000 \
--ignore-vuln PYSEC-2023-242 \
--skip-editable\""
That’s it! Once you reload your shell, all you have to do is type is pip-audit-proj when you’re in the root of your project that contains your requirements.txt file. You should see something like this below. Slow the first time, fast afterwards.

Protecting Docker in production too
Let’s handle one more situation while we are at it. You’re running your Python app IN Docker. Part of the Docker build configures the image and installs your dependencies. We can add a pip-audit check there too:
# Dockerfile for your app (different than validation image above)
# All the steps to copy your app over and configure the image ...
# After creating a venv in /venv and copying your requirements.txt to /app
# Check for any sketchy packages.
# We are using mount rather than a volume because
# we want to cache build time activity, not runtime activity.
RUN --mount=type=cache,target=/root/.cache uv pip install --python /venv/bin/python3 --upgrade pip-audit
RUN --mount=type=cache,target=/root/.cache /venv/bin/pip-audit --ignore-vuln CVE-2025-53000 --ignore-vuln PYSEC-2023-242 --skip-editable
# ENTRYPOINT ... for your app
Conclusion
There you have it. Two birds, one Docker stone for both. Our first Dockerfile built a reusable Docker image named pipauditdocker to run isolated tests against a requirements file. This second one demonstrates how we can make our docker/docker compose build completely fail if there is a bad dependency saving us from letting it slip into production.
Cheers
Michael
December 26, 2025 11:53 PM UTC
Seth Michael Larson
Getting started with Playdate on Ubuntu đš
December 26, 2025 12:00 AM UTC
December 25, 2025
Seth Michael Larson
Blind Carbon Copy (BCC) for SMS
December 25, 2025 12:00 AM UTC
December 24, 2025
Real Python
LlamaIndex in Python: A RAG Guide With Examples
Learn how to set up LlamaIndex, choose an LLM, load your data, build and persist an index, and run queries to get grounded, reliable answers with examples.
December 24, 2025 02:00 PM UTC
Quiz: LlamaIndex in Python: A RAG Guide With Examples
Take this Python LlamaIndex quiz to test your understanding of index persistence, reloading, and performance gains in RAG applications.
December 24, 2025 12:00 PM UTC
December 23, 2025
PyCoderâs Weekly
Issue #714: Narwhals, Selenium, Testing Conundrum, and More (Dec. 23, 2025)
December 23, 2025 07:30 PM UTC
Reuven Lerner
Reuvenâs 2025 in review
Can you believe that 2025 is almost over? It was full of big events for me, and yet it also whizzed past at breakneck speed. And so, before we start 2026, I want to share a whole bunch of updates on what I’ve done over the last 12 months â and where I plan to […]
The post Reuven’s 2025 in review appeared first on Reuven Lerner.
December 23, 2025 02:44 PM UTC
Real Python
Reading User Input From the Keyboard With Python
Master taking user input in Python to build interactive terminal apps with clear prompts, solid error handling, and smooth multi-step flows.
December 23, 2025 02:00 PM UTC
Hugo van Kemenade
And now for something completely different
Starting in 2019, Python 3.8 and 3.9 release manager Ćukasz Langa added a new section to the release notes called “And now for something completely different” with a sketch transcript from Monty Python.
For Python 3.10 and 3.11, the next release manager Pablo Galindo Salgado continued the section but included astrophysics facts.
For Python 3.12, the next RM Thomas Wouters shared poems (and took a break for 3.13).
And for Python 3.14, I’m doing all things Ï, pie and [mag]pie.
Here’s a collection of my different things for the first year (and a bit) of Python 3.14.
alpha 1 #
Ï (or pi) is a mathematical constant, approximately 3.14, for the ratio of a circleâs circumference to its diameter. It is an irrational number, which means it cannot be written as a simple fraction of two integers. When written as a decimal, its digits go on forever without ever repeating a pattern.
Hereâs 76 digits of Ï:
3.141592653589793238462643383279502884197169399375105820974944592307816406286
Piphilology is the creation of mnemonics to help remember digits of Ï.
In a pi-poem, or âpiemâ, the number of letters in a word equal the corresponding digit. This covers 9 digits, 3.14159265:
How I wish I could recollect pi easily today!
One of the most well-known covers 15 digits, 3.14159265358979:
How I want a drink, alcoholic of course, after the heavy chapters involving quantum mechanics!
Hereâs a 35-word piem in the shape of a circle, 3.1415926535897932384626433832795728:
Itâs a fact A ratio immutable Of circle round and width, Produces geometryâs deepest conundrum. For as the numerals stay random, No repeat lets out its presence, Yet it forever stretches forth. Nothing to eternity.
The Guinness World Record for memorising the most digits is held by Rajveer Meena, who recited 70,000 digits blindfold in 2015. The unofficial record is held by Akira Haraguchi who recited 100,000 digits in 2006.
alpha 2 #
Ludolph van Ceulen (1540-1610) was a fencing and mathematics teacher in Leiden, Netherlands, and spent around 25 years calculating Ï (or pi), using essentially the same methods Archimedes employed some seventeen hundred years earlier.
Archimedes estimated Ï by calculating the circumferences of polygons that fit just inside and outside of a circle, reasoning the circumference of the circle lies between these two values. Archimedes went up to polygons with 96 sides, for a value between 3.1408 and 3.1428, which is accurate to two decimal places.
Van Ceulen used a polygon with half a billion sides. He published a 20-decimal value in his 1596 book Vanden Circkel (âOn the Circleâ), and later expanded it to 35 decimals:
3.14159265358979323846264338327950288
Van Ceulenâs 20 digits is more than enough precision for any conceivable practical purpose. For example, even if a printed circle was perfect down to the atomic scale, the thermal vibrations of the molecules of ink would make most of those digits physically meaningless. NASA Jet Propulsion Laboratoryâs highest accuracy calculations, for interplanetary navigation, uses 15 decimals: 3.141592653589793.
At Van Ceulenâs request, his upper and lower bounds for Ï were engraved on his tombstone in Leiden. The tombstone was eventually lost but restored in 2000. In the Netherlands and Germany, Ï is sometimes referred to as the âLudolphine numberâ, after Van Ceulen.
alpha 3 #
A mince pie is a small, round covered tart filled with âmincemeatâ, usually eaten during the Christmas season â the UK consumes some 800 million each Christmas. Mincemeat is a mixture of things like apple, dried fruits, candied peel and spices, and originally would have contained meat chopped small, but rarely nowadays. They are often served warm with brandy butter.
According to the Oxford English Dictionary, the earliest mention of Christmas mince pies is by Thomas Dekker, writing in the aftermath of the 1603 London plague, in Newes from Graues-end: Sent to Nobody (1604):
Ten thousand in London swore to feast their neighbors with nothing but plum-porredge, and mince-pyes all Christmas.
Hereâs a meaty recipe from Rare and Excellent Receipts, Experiencâd and Taught by Mrs Mary Tillinghast and now Printed for the Use of her Scholars Only (1678):
XV. How to make Mince-pies.
To every pound of Meat, take two pound of beef Suet, a pound of Corrants, and a quarter of an Ounce of Cinnamon, one Nutmeg, a little beaten Mace, some beaten Colves, a little Sack & Rose-water, two large Pippins, some Orange and Lemon peel cut very thin, and shred very small, a few beaten Carraway-seeds, if you love them the Juyce of half a Lemon squezâd into this quantity of meat; for Sugar, sweeten it to your relish; then mix all these together and fill your Pie. The best meat for Pies is Neats-Tongues, or a leg of Veal; you may make them of a leg of Mutton if you please; the meat must be parboylâd if you do not spend it presently; but if it be for present use, you may do it raw, and the Pies will be the better.
alpha 4 #
In Python, you can use Greek letters as constants. For example:
from math import pi as Ï
def circumference(radius: float) -> float:
return 2 * Ï * radius
print(circumference(6378.137)) # 40075.016685578485
alpha 5 #
2025-01-29 marked the start of a new lunar year, the Year of the Snake :snake: (and the Year of Python?).
For centuries, Ï was often approximated as 3 in China. Some time between the years 1 and 5 CE, astronomer, librarian, mathematician and politician Liu Xin (ćæ) calculated Ï as 3.154.
Around 130 CE, mathematician, astronomer, and geographer Zhang Heng (ćŒ”èĄĄ, 78â139) compared the celestial circle with the diameter of the earth as 736:232 to get 3.1724. He also came up with a formula for the ratio between a cube and inscribed sphere as 8:5, implying the ratio of a squareâs area to an inscribed circle is â8:â5. From this, he calculated Ï as â10 (~3.162).
Third century mathematician Liu Hui (ććŸœ) came up with an algorithm for calculating Ï iteratively: calculate the area of a polygon inscribed in a circle, then as the number of sides of the polygon is increased, the area becomes closer to that of the circle, from which you can approximate Ï.
This algorithm is similar to the method used by Archimedes in the 3rd century BCE and Ludolph van Ceulen in the 16th century CE (see 3.14.0a2 release notes), but Archimedes only went up to a 96-sided polygon (96-gon). Liu Hui went up to a 192-gon to approximate Ï as 157/50 (3.14) and later a 3072-gon for 3.14159.
Liu Hu wrote a commentary on the book The Nine Chapters on the Mathematical Art which included his Ï approximations.
In the fifth century, astronomer, inventor, mathematician, politician, and writer Zu Chongzhi (ç„æČäč, 429â500) used Liu Huiâs algorithm to inscribe a 12,288-gon to compute Ï between 3.1415926 and 3.1415927, correct to seven decimal places. This was more accurate than Hellenistic calculations and wouldnât be improved upon for 900 years.
Happy Year of the Snake!
alpha 6 #
March 14 is celebrated as pi day, because 3.14 is an approximation of Ï. The day is observed by eating pies (savoury and/or sweet) and celebrating Ï. The first pi day was organised by physicist and tinkerer Larry Shaw of the San Francisco Exploratorium in 1988. It is also the International Day of Mathematics and Albert Einsteinâs birthday. Letâs all eat some pie, recite some Ï, install and test some py, and wish a happy birthday to Albert, Loren and all the other pi day children!
alpha 7 #
On Saturday, 5th April, 3.141592653589793 months of the year had elapsed.
beta 1 #
The mathematical constant pi is represented by the Greek letter Ï and represents the ratio of a circleâs circumference to its diameter. The first person to use Ï as a symbol for this ratio was Welsh self-taught mathematician William Jones in 1706. He was a farmerâs son born in Llanfihangel Treâr Beirdd on Angelsy (Ynys MĂŽn) in 1675 and only received a basic education at a local charity school. However, the owner of his parentsâ farm noticed his mathematical ability and arranged for him to move to London to work in a bank.
By age 20, he served at sea in the Royal Navy, teaching sailors mathematics and helping with the shipâs navigation. On return to London seven years later, he became a maths teacher in coffee houses and a private tutor. In 1706, Jones published Synopsis Palmariorum Matheseos which used the symbol Ï for the ratio of a circleâs circumference to diameter (hunt for it on pages 243 and 263 or here). Jones was also the first person to realise Ï is an irrational number, meaning it can be written as decimal number that goes on forever, but cannot be written as a fraction of two integers.
But why Ï? Itâs thought Jones used the Greek letter Ï because itâs the first letter in perimetron or perimeter. Jones was the first to use Ï as our familiar ratio but wasnât the first to use it in as part of the ratio. William Oughtred, in his 1631 Clavis Mathematicae (The Key of Mathematics), used Ï/ÎŽ to represent what we now call pi. His Ï was the circumference, not the ratio of circumference to diameter. James Gregory, in his 1668 Geometriae Pars Universalis (The Universal Part of Geometry) used Ï/Ï instead, where Ï is the radius, making the ratio 6.28⊠or Ï. After Jones, Leonhard Euler had used Ï for 6.28âŠ, and also p for 3.14âŠ, before settling on and popularising Ï for the famous ratio.
beta 2 #
In 1897, the State of Indiana almost passed a bill defining Ï as 3.2.
Of course, itâs not that simple.
Edwin J. Goodwin, M.D., claimed to have come up with a solution to an ancient geometrical problem called squaring the circle, first proposed in Greek mathematics. It involves trying to draw a circle and a square with the same area, using only a compass and a straight edge. It turns out to be impossible because Ï is transcendental (and this had been proved just 13 years earlier by Ferdinand von Lindemann), but Goodwin fudged things so the value of Ï was 3.2 (his writings have included at least nine different values of Ï: including 4, 3.236, 3.232, 3.2325⊠and even 9.2376âŠ).
Goodwin had copyrighted his proof and offered it to the State of Indiana to use in their educational textbooks without paying royalties, provided they endorsed it. And so Indiana Bill No. 246 was introduced to the House on 18th January 1897. It was not understood and initially referred to the House Committee on Canals, also called the Committee on Swamp Lands. They then referred it to the Committee on Education, who duly recommended on 2nd February that âsaid bill do passâ. It passed its second reading on the 5th and the education chair moved that they suspend the constitutional rule that required bills to be read on three separate days. This passed 72-0, and the bill itself passed 67-0.
The bill was referred to the Senate on 10th February, had its first reading on the 11th, and was referred to the Committee on Temperance, whose chair on the 12th recommended âthat said bill do passâ.
A mathematics professor, Clarence Abiathar Waldo, happened to be in the State Capitol on the day the House passed the bill and walked in during the debate to hear an ex-teacher argue:
The case is perfectly simple. If we pass this bill which establishes a new and correct value for pi , the author offers to our state without cost the use of his discovery and its free publication in our school text books, while everyone else must pay him a royalty.
Waldo ensured the senators were âproperly coachedâ; and on the 12th, during the second reading, after an unsuccessful attempt to amend the bill it was postponed indefinitely. But not before the senators had some fun.
The Indiana News reported on the 13th:
âŠthe bill was brought up and made fun of. The Senators made bad puns about it, ridiculed it and laughed over it. The fun lasted half an hour. Senator Hubbell said that it was not meet for the Senate, which was costing the State $250 a day, to waste its time in such frivolity. He said that in reading the leading newspapers of Chicago and the East, he found that the Indiana State Legislature had laid itself open to ridicule by the action already taken on the bill. He thought consideration of such a proposition was not dignified or worthy of the Senate. He moved the indefinite postponement of the bill, and the motion carried.
beta 3 #
If youâre heading out to sea, remember the Maritime Approximation:
Ï mph = e knots
beta 4 #
All this talk of Ï and yet some say Ï is wrong. Tau Day (June 28th, 6/28 in the US) celebrates Ï as the âtrue circle constantâ, as the ratio of a circleâs circumference to its radius, C/r = 6.283185⊠The Tau Manifesto declares Ï âa confusing and unnatural choice for the circle constantâ, in part because â2Ï occurs with astonishing frequency throughout mathematicsâ.
If you wish to embrace Ï the good news is PEP 628
added math.tau to Python 3.6
in 2016:
When working with radians, it is trivial to convert any given fraction of a circle to a value in radians in terms of
tau. A quarter circle istau/4, a half circle istau/2, seven 25ths is7*tau/25, etc. In contrast with the equivalent expressions in terms ofpi(pi/2,pi,14*pi/25), the unnecessary and needlessly confusing multiplication by two is gone.
release candidate 1 #
Today, 22nd July, is Pi Approximation Day, because 22/7 is a common approximation of Ï and closer to Ï than 3.14.
22/7 is a Diophantine approximation, named after Diophantus of Alexandria (3rd century CE), which is a way of estimating a real number as a ratio of two integers. 22/7 has been known since antiquity; Archimedes (3rd century BCE) wrote the first known proof that 22/7 overestimates Ï by comparing 96-sided polygons to the circle it circumscribes.
Another approximation is 355/113. In Chinese mathematics, 22/7 and 355/113 are respectively known as YuelĂŒ (çșŠç; yuÄlÇ; âapproximate ratioâ) and MilĂŒ (ćŻç; mĂŹlÇ; âclose ratioâ).
Happy Pi Approximation Day!
release candidate 2 #
The magpie, Pica pica in Latin, is a black and white bird in the crow family, known for its chattering call.
The first-known use in English is from a 1589 poem, where magpie is spelled âmagpyâ and cuckoo is âcookowâ:
Th[e]y fly to wood like breeding hauke, And leave old neighbours loue, They pearch themselves in syluane lodge, And soare in thâ aire aboue. There : magpy teacheth them to chat, And cookow soone doth hit them pat.
The name comes from Mag, short for Margery or Margaret (compare robin redbreast, jenny wren, and its corvid relative jackdaw); and pie, a magpie or other bird with black and white (or pied) plumage. The sea-pie (1552) is the oystercatcher, the grey pie (1678) and murdering pie (1688) is the great grey shrike. Others birds include the yellow and black pie, red-billed pie, wandering tree-pie, and river pie. The rain-pie, wood-pie and French pie are woodpeckers.
Pie on its own dates to before 1225, and comes from the Latin name for the bird, pica.
release candidate 3 #
According to Pablo Galindo Salgado at PyCon Greece:
There are things that are supercool indeed, like for instance, this is one of the results that I’m more proud about. This equation over here, which you don’t need to understand, you don’t need to be scared about, but this equation here tells what is the maximum time that it takes for a ray of light to fall into a black hole. And as you can see the math is quite complicated but the answer is quite simple: it’s 2Ï times the mass of the black hole. So if you normalise by the mass of the black hole, the answer is 2Ï. And because there is nothing specific about your election of things in this formula, this formula is universal. It means it doesn’t depend on anything other than nature itself. Which means that you can use this as a definition of Ï. This is a valid alternative definition of the number Ï. It’s literally half the maximum time it takes to fall into a black hole, which is kind of crazy. So next time someone asks you what Ï means you can just drop this thing and impress them quite a lot. Maybe Hugo could use this information to put it into the release notes of Ïthon [yes, I can, thank you!].
3.14.0 (final) #
Edgar Allen Poe died on 7th October 1849.
As we all recall from 3.14.0a1, piphilology is the creation of mnemonics to help memorise the digits of Ï, and the number of letters in each word in a pi-poem (or âpiemâ) successively correspond to the digits of Ï.
In 1995, Mike Keith, an American mathematician and author of constrained writing, retold Poeâs The Raven as a 740-word piem. Hereâs the first two stanzas of Near A Raven:
Poe, E. Near a Raven
Midnights so dreary, tired and weary. Silently pondering volumes extolling all by-now obsolete lore. During my rather long nap - the weirdest tap! An ominous vibrating sound disturbing my chamberâs antedoor. âThisâ, I whispered quietly, âI ignoreâ.
Perfectly, the intellect remembers: the ghostly fires, a glittering ember. Inflamed by lightningâs outbursts, windows cast penumbras upon this floor. Sorrowful, as one mistreated, unhappy thoughts I heeded: That inimitable lesson in elegance - Lenore - Is delighting, excitingâŠnevermore.
3.14.1 #
Seki Takakazu (éą ćć; c. March 1642 â December 5, 1708) was a Japanese mathematician and samurai who laid the foundations of Japanese mathematics, later known as wasan (ćçź, from wa (âJapaneseâ) and san (âcalculationâ).
Seki was a contemporary of Isaac Newton and Gottfried Leibniz but worked independently. He created a new algebraic system, worked on infinitesimal calculus, and is credited with the discovery of Bernoulli numbers (before Bernoulliâs birth).
Seki also calculated Ï to 11 decimal places using a polygon with 131,072 sides inscribed within a circle, using an acceleration method now known as Aitkenâs delta-squared process, which was rediscovered by Alexander Aitken in 1926.
Header photo: A scan of Seki Takakazu’s posthumous KatsuyĆ SanpĆ (1712) showing calculations of Ï.
December 23, 2025 01:03 PM UTC
Real Python
Quiz: Recursion in Python: An Introduction
Test your understanding of recursion in Python, including base cases, recursive structure, performance considerations, and common use cases.
December 23, 2025 12:00 PM UTC
"Michael Kennedy's Thoughts on Technology"
Python Supply Chain Security Made Easy
Maybe you’ve heard that hackers have been trying to take advantage of open source software to inject code into your machine, and worst case scenario, even the consumers of your libraries or your applications machines. In this quick post, I’ll show you how to integrate Python’s “Official” package scanning technology directly into your continuous integration and your project’s unit tests. While pip-audit is maintained in part by Trail of Bits with support from Google, it’s part of the PyPA organization.
Why this matters
Here are 5 recent, high-danger PyPI security issues supply chain attacks where âpip installâ can turn into âpip install a backdoor.â Afterwards, we talk about how to scan for and prevent these from making it to your users.
Compromised popular package release (ultralytics), malicious update pushed to PyPI
What happened: A malicious version (8.3.41) of the widely-used ultralytics package was published to PyPI, containing code that downloaded the XMRig coinminer. Follow-on versions also carried the malicious downloader, and the writeup attributes the initial compromise to a GitHub Actions script injection, plus later abuse consistent with a stolen PyPI API token. Source: ReversingLabs
Campaign of fake packages stealing cloud access tokens, 14,100+ downloads before removal
What happened: Researchers reported multiple bogus PyPI libraries (including âtime-related utilitiesâ) designed to exfiltrate cloud access tokens, with the campaign exceeding 14,100 downloads before takedown. If those tokens are real, this can turn into cloud account takeover. Source: The Hacker News
Typosquatting and name-confusion targeting colorama, with remote control and data theft payloads
What happened: A campaign uploaded lookalike package names to PyPI to catch developers intending to install colorama, with payloads described as enabling persistent remote access/remote control plus harvesting and exfiltration of sensitive data. High danger mainly because colorama is popular and typos happen. Source: Checkmarx
PyPI credential-phishing led to real account compromise and malicious releases of a legit project (num2words)
What happened: PyPI reported an email phishing campaign using a lookalike domain; 4 accounts were successfully phished, attacker-generated API tokens were revoked, and malicious releases of num2words were uploaded then removed. This is the âsteal maintainer creds, ship malware via trusted package nameâ playbook. Source: Python Package Index Blog
SilentSync RAT delivered via malicious PyPI packages (sisaws, secmeasure)
What happened: Zscaler documented malicious packages (including typosquatting) that deliver a Python-based remote access trojan (RAT) with command execution, file exfiltration, screen capture, and browser data theft (credentials, cookies, etc.). Source: Zscaler
Integrating pip-audit
Those are definitely scary situations. I’m sure you’ve heard about typo squatting and how annoying that can be. Caution will save you there. Where caution will not save you is when a legitimate package has its supply chain taken over. A lot of times this could look like a package that you use depends on another package whose maintainer was phished. And now everything that uses that library is carrying that vulnerability forward.
Enter pip-audit.
pip-audit is great because you can just run it on the command line. It will check against PyPA’s official list of vulnerabilities and tell you if anything in your virtual environment or requirements files is known to be malicious.
You could even set up a GitHub Action to do so, and I wouldn’t recommend against that at all. But it’s also valuable to make this check happen on developers’ machines. It’s a simple two-step process to do so:
- Add pip-audit to your project’s development dependencies or install it globally with
uv tool install pip-audit. - Create a unit test that simply shells out to execute pip-audit and fails the test if an issue is found.
Part one’s easy. Part two takes a little bit more work. That’s okay, because I got it for you. Just download the file here and drop it in your pytest test directory:
Here’s a small segment to give you a sense of what’s involved.
def test_pip_audit_no_vulnerabilities():
# setup ...
# Run pip-audit with JSON output for easier parsing
try:
result = subprocess.run(
[
sys.executable,
'-m',
'pip_audit',
'--format=json',
'--progress-spinner=off',
'--ignore-vuln',
'CVE-2025-53000', # example of skipping an irrelevant cve
'--skip-editable', # don't test your own package in dev
],
cwd=project_root,
capture_output=True,
text=True,
timeout=120, # 2 minute timeout
)
except subprocess.TimeoutExpired:
pytest.fail('pip-audit command timed out after 120 seconds')
except FileNotFoundError:
pytest.fail('pip-audit not installed or not accessible')
That’s it! When anything runs your unit test, whether that’s continuous integration, a git hook, or just a developer testing their code, you’ll also run a pip-audit audit of your project.

Let others find out
Now, pip-audit tests if a malicious package has been installed, In which case, for that poor developer or machine, it may be too late. If it’s CI, who cares? But one other feature you can combine with this that is really nice is uv’s ability to put a delay on upgrading your dependencies.
Many developers, myself included, will typically run some kind of command that will pin your versions. Periodically we also run a command that looks for newer libraries and updates pinned versions so we’re using the latest code. So this way you upgrade in a stair-step manner at the time you’re intending to change versions.
This works great. However, what if the malicious version of a package is released five minutes before before you run this command. You’re getting it installed. But pretty soon, the community is going to find out that something is afoot, report it, and it will be yanked from PyPI. Here bad timing got you hacked.
While it’s not a guaranteed solution, certainly Defense In Depth would tell us maybe wait a few days to install a package. But you don’t want to review packages manually one by one, do you? For example, for Talk Python Training, we have over 200 packages for that website. It would be an immense hassle to verify the dates of each one and manually pick the versions.
No need! We can just add a simple delay to our uv command:
uv pip compile requirements.piptools --upgrade --output-file requirements.txt --exclude-newer "1 week"
In particular, notice –exclude-newer “1 week”. The exact duration isn’t the important thing. It’s about putting a little bit of a delay for issues to be reported into your workflow. You can read about the full feature here. This way, we only incorporate packages that have survived in the public on PyPI for at least one week.
Part 2
Be sure to check out the follow up post DevOps Python Supply Chain Security for even more tips.
Hope this helps. Stay safe out there.
December 23, 2025 12:16 AM UTC
Armin Ronacher
Advent of Slop: A Guest Post by Claude
December 23, 2025 12:00 AM UTC
December 22, 2025
EuroPython Society
EPS Board 2025-2026
We’re happy to announce our new board for the 2025-2026 term:
- Artur Czepiel (Chair)
- Mia Bajić (Vice Chair)
- Anders Hammarquist
- Angel Ramboi
- Aris Nivorlis
- Ege Akman
- Yuliia Barabash
You can read more about them in their nomination post at https://www.europython-society.org/list-of-eps-board-candidates-for-2025-2026/. The minutes and
December 22, 2025 11:21 PM UTC
Giampaolo Rodola
Detect memory leaks of C extensions with psutil and psleak
Memory leaks in Python are often straightforward to diagnose. Just look at RSS, track Python object counts, follow reference graphs. But leaks inside C extension modules are another story. Traditional memory metrics such as RSS and VMS frequently fail to reveal them because Python's memory allocator sits above the platform's âŠ
December 22, 2025 11:00 PM UTC
Real Python
SOLID Design Principles: Improve Object-Oriented Code in Python
Learn how to apply SOLID design principles in Python and build maintainable, reusable, and testable object-oriented code.
December 22, 2025 02:00 PM UTC
Quiz: SOLID Design Principles: Improve Object-Oriented Code in Python
Learn Liskov substitution in Python. Spot Square and Rectangle pitfalls and design safer APIs with polymorphism. Test your understanding now.
December 22, 2025 12:00 PM UTC
Nicola Iarocci
Rediscovering a 2021 podcast on Python, .NET, and open source
Yesterday, the kids came home for the Christmas holidays. Marco surprised me by telling me that on his flight from Brussels, he discovered and listened to âmy podcastâ on Spotify. I was stunned. I didn’t remember ever recording a podcast, even though I’ve given a few interviews here and there over the years.
During my usual morning walk today, I went to look for it, and there it was, an interview I had done in 2021 that I had completely forgotten about. I got over the initial embarrassment (it’s always strange to hear your own voice) and resisted the temptation to turn it off, listening to it all the way through. I must admit that it captures that moment in my professional life, and much of the content is still relevant, especially regarding my experience as an open-source author and maintainer and my transition from C# to Python and back.
December 22, 2025 09:49 AM UTC
Python Bytes
#463 2025 is @wrapped
Topics include Has the cost of building software just dropped 90%?, , How FOSS Won and Why It Matters, and.
December 22, 2025 08:00 AM UTC
Zato Blog
Modern REST API Tutorial in Python
Modern REST API Tutorial in Python
Great APIs don't win theoretical arguments - they just prefer to work reliably and to make developers' lives easier.
Here's a tutorial on what building production APIs is really about: creating interfaces that are practical in usage, while keeping your systems maintainable for years to come.
Sound intriguing? Read the modern REST API tutorial in Python here.
More resources
†Python API integration tutorials
†What is a Network Packet Broker? How to automate networks in Python?
†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 Python
December 22, 2025 03:00 AM UTC
Armin Ronacher
A Year Of Vibes
December 22, 2025 12:00 AM UTC
Seth Michael Larson
PEP 770 Software Bill-of-Materials (SBOM) data from PyPI, Fedora, and Red Hat
December 22, 2025 12:00 AM UTC
December 21, 2025
Ned Batchelder
Generating data shapes with Hypothesis
In my last blog post (A testing conundrum), I described
trying to test my Hasher class which hashes nested data. I couldn’t get
Hypothesis to generate usable data for my test. I wanted to assert that two
equal data items would hash equally, but Hypothesis was finding pairs like
[0] and [False]. These are equal but hash differently because the
hash takes the types into account.
In the blog post I said,
If I had a schema for the data I would be comparing, I could use it to steer Hypothesis to generate realistic data. But I don’t have that schema...
I don’t want a fixed schema for the data Hasher would accept, but tests to compare data generated from the same schema. It shouldn’t compare a list of ints to a list of bools. Hypothesis is good at generating things randomly. Usually it generates data randomly, but we can also use it to generate schemas randomly!
Hypothesis basics
Before describing my solution, I’ll take a quick detour to describe how Hypothesis works.
Hypothesis calls their randomness machines “strategies”. Here is a strategy that will produce random integers between -99 and 1000:
import hypothesis.strategies as st
st.integers(min_value=-99, max_value=1000)
Strategies can be composed:
st.lists(st.integers(min_value=-99, max_value=1000), max_size=50)
This will produce lists of integers from -99 to 1000. The lists will have up to 50 elements.
Strategies are used in tests with the @given decorator, which takes a
strategy and runs the test a number of times with different example data drawn
from the strategy. In your test you check a desired property that holds true
for any data the strategy can produce.
To demonstrate, here’s a test of sum() that checks that summing a list of numbers in two halves gives the same answer as summing the whole list:
from hypothesis import given, strategies as st
@given(st.lists(st.integers(min_value=-99, max_value=1000), max_size=50))
def test_sum(nums):
# We don't have to test sum(), this is just an example!
mid = len(nums) // 2
assert sum(nums) == sum(nums[:mid]) + sum(nums[mid:])
By default, Hypothesis will run the test 100 times, each with a different randomly generated list of numbers.
Schema strategies
The solution to my data comparison problem is to have Hypothesis generate a random schema in the form of a strategy, then use that strategy to generate two examples. Doing this repeatedly will get us pairs of data that have the same “shape” that will work well for our tests.
This is kind of twisty, so let’s look at it in pieces. We start with a list of strategies that produce primitive values:
primitives = [
st.none(),
st.booleans(),
st.integers(min_value=-1000, max_value=10_000_000),
st.floats(min_value=-100, max_value=100),
st.text(max_size=10),
st.binary(max_size=10),
]
Then a list of strategies that produce hashable values, which are all the primitives, plus tuples of any of the primitives:
def tuples_of(elements):
"""Make a strategy for tuples of some other strategy."""
return st.lists(elements, max_size=3).map(tuple)
# List of strategies that produce hashable data.
hashables = primitives + [tuples_of(s) for s in primitives]
We want to be able to make nested dictionaries with leaves of some other type. This function takes a leaf-making strategy and produces a strategy to make those dictionaries:
def nested_dicts_of(leaves):
"""Make a strategy for recursive dicts with leaves from another strategy."""
return st.recursive(
leaves,
lambda children: st.dictionaries(st.text(max_size=10), children, max_size=3),
max_leaves=10,
)
Finally, here’s our strategy that makes schema strategies:
nested_data_schemas = st.recursive(
st.sampled_from(primitives),
lambda children: st.one_of(
children.map(lambda s: st.lists(s, max_size=5)),
children.map(tuples_of),
st.sampled_from(hashables).map(lambda s: st.sets(s, max_size=10)),
children.map(nested_dicts_of),
),
max_leaves=3,
)
For debugging, it’s helpful to generate an example strategy from this strategy, and then an example from that, many times:
for _ in range(50):
print(repr(nested_data_schemas.example().example()))
Hypothesis is good at making data we’d never think to try ourselves. Here is some of what it made:
[None, None, None, None, None]
{}
[{False}, {False, True}, {False, True}, {False, True}]
{(1.9, 80.64553337755876), (-41.30770818038395, 9.42967906108538, -58.835811641800085), (31.102786990742203,), (28.2724197133397, 6.103515625e-05, -84.35107066147154), (7.436329211943294e-263,), (-17.335739410320514, 1.5029061311609365e-292, -8.17077562035881), (-8.029363284353857e-169, 49.45840191722425, -15.301768150196054), (5.960464477539063e-08, 1.1518373121077722e-213), (), (-0.3262457914511714,)}
[b'+nY2~\xaf\x8d*\xbb\xbf', b'\xe4\xb5\xae\xa2\x1a', b'\xb6\xab\xafEi\xc3C\xab"\xe1', b'\xf0\x07\xdf\xf5\x99', b'2\x06\xd4\xee-\xca\xee\x9f\xe4W']
{'fV': [81.37177374286324, 3.082323424992609e-212, 3.089885728465406e-151, -9.51475773638932e-86, -17.061851038597922], 'J»\x0c\x86è|\x88\x03\x8aU': [29.549966208819654]}
[{}, -68.48316192397687]
None
['\x85\U0004bf04°', 'pB\x07iQT', 'TRUE', '\x1a5ĂčZĂą\U00048752+Âč\U0005fdf8ĂȘ', '\U000fe0b9m*€\U000b9f1e']
(14.232866652585258, -31.193835515904652, 62.29850355163285)
{'': {'': None, 'Ă\U000be8de§\nĂ\U00093608u': None, 'Y\U000709e4„ĂčU)GE\U000dddc5ÂŹ': None}}
[{(), (b'\xe7', b'')}, {(), (b'l\xc6\x80\xdf\x16\x91', b'', b'\x10,')}, {(b'\xbb\xfb\x1c\xf6\xcd\xff\x93\xe0\xec\xed',), (b'g',), (b'\x8e9I\xcdgs\xaf\xd1\xec\xf7', b'\x94\xe6#', b'?\xc9\xa0\x01~$k'), (b'r', b'\x8f\xba\xe6\xfe\x92n\xc7K\x98\xbb', b'\x92\xaa\xe8\xa6s'), (b'f\x98_\xb3\xd7', b'\xf4+\xf7\xbcU8RV', b'\xda\xb0'), (b'D',), (b'\xab\xe9\xf6\xe9', b'7Zr\xb7\x0bl\xb6\x92\xb8\xad', b'\x8f\xe4]\x8f'), (b'\xcf\xfb\xd4\xce\x12\xe2U\x94mt',), (b'\x9eV\x11', b'\xc5\x88\xde\x8d\xba?\xeb'), ()}, {(b'}', b'\xe9\xd6\x89\x8b')}, {(b'\xcb`', b'\xfd', b'w\x19@\xee'), ()}]
((), (), ())
Finally writing the test
Time to use all of this in a test:
@given(nested_data_schemas.flatmap(lambda s: st.tuples(s, s)))
def test_same_schema(data_pair):
data1, data2 = data_pair
h1, h2 = Hasher(), Hasher()
h1.update(data1)
h2.update(data2)
if data1 == data2:
assert h1.digest() == h2.digest()
else:
# Strictly speaking, unequal data could produce equal hashes,
# but it's very unlikely, so test for it anyway.
assert h1.digest() != h2.digest()
Here I use the .flatmap() method to draw an example from the
nested_data_schemas strategy and call the provided lambda with the drawn
example, which is itself a strategy. The lambda uses st.tuples to make
tuples with two examples drawn from the strategy. So we get one data schema, and
two examples from it as a tuple passed into the test as data_pair. The
test then unpacks the data, hashes them, and makes the appropriate
assertion.
This works great: the tests pass. To check that the test was working well, I made some breaking tweaks to the Hasher class. If Hypothesis is configured to generate enough examples, it finds data examples demonstrating the failures.
I’m pleased with the results. Hypothesis is something I’ve been wanting to use more, so I’m glad I took this chance to learn more about it and get it working for these tests. To be honest, this is way more than I needed to test my Hasher class. But once I got started, I wanted to get it right, and learning is always good.
I’m a bit concerned that the standard setting (100 examples) isn’t enough to find the planted bugs in Hasher. There are many parameters in my strategies that could be tweaked to keep Hypothesis from wandering too broadly, but I don’t know how to decide what to change.
Actually
The code in this post is different than the actual code I ended up with.
Mostly this is because I was working on the code while I was writing this post,
and discovered some problems that I wanted to fix. For example, the
tuples_of function makes homogeneous tuples: varying lengths with
elements all of the same type. This is not the usual use of tuples (see
Lists vs. Tuples). Adapting for heterogeneous tuples added
more complexity, which was interesting to learn, but I didn’t want to go back
and add it here.
You can look at the final strategies.py to see that and other details, including type hints for everything, which was a journey of its own.
Postscript: AI assistance
I would not have been able to come up with all of this by myself. Hypothesis is very powerful, but requires a new way of thinking about things. It’s twisty to have functions returning strategies, and especially strategies producing strategies. The docs don’t have many examples, so it can be hard to get a foothold on the concepts.
Claude helped me by providing initial code, answering questions, debugging when things didn’t work out, and so on. If you are interested, this is one of the discussions I had with it.
December 21, 2025 04:43 PM UTC
December 19, 2025
Luke Plant
Help my website is too small
How can it be a real website if itâs less than 7k?
December 19, 2025 01:45 PM UTC
Real Python
The Real Python Podcast â Episode #277: Moving Towards Spec-Driven Development
What are the advantages of spec-driven development compared to vibe coding with an LLM? Are these recent trends a move toward declarative programming? This week on the show, Marc Brooker, VP and Distinguished Engineer at AWS, joins us to discuss specification-driven development and Kiro.
December 19, 2025 12:00 PM UTC
December 18, 2025
Django Weblog
Hitting the Home Stretch: Help Us Reach the Django Software Foundation's Year-End Goal!
As we wrap up another strong year for the Django community, we wanted to share an update and a thank you. This year, we raised our fundraising goal from $200,000 to $300,000, and we are excited to say we are now over 88% of the way there. That puts us firmly in the home stretch, and a little more support will help us close the gap and reach 100%.
So why the higher goal this year? We expanded the Django Fellows program to include a third Fellow. In August, we welcomed Jacob Tyler Walls as our newest Django Fellow. That extra capacity gives the team more flexibility and resilience, whether someone is taking parental leave, time off around holidays, or stepping away briefly for other reasons. It also makes it easier for Fellows to attend more Django events and stay connected with the community, all while keeping the project running smoothly without putting too much pressure on any one person.
We are also preparing to raise funds for an executive director role early next year. That work is coming soon, but right now, the priority is finishing this year strong.
We want to say a sincere thank you to our existing sponsors and to everyone who has donated so far. Your support directly funds stable Django releases, security work, community programs, and the long-term health of the framework. If you or your organization have end-of-year matching funds or a giving program, this is a great moment to put them to use and help push us past the finish line.
If you would like to help us reach that final stretch, you can find all the details on our fundraising page
Other ways to support Django:
- Benevity Workplace Giving Program: If your employer participates, you can make donations to the DSF via payroll deduction.
- Sponsor Django via GitHub Sponsors: Support Django directly through GitHub's sponsorship platform.
- Official Merch Store: Buy official t-shirts, accessories, and more to support Django.
Thank you for helping support Django and the people who make it possible. We are incredibly grateful for this community and everything you do to keep Django strong.
