One of the best things about maintaining open source in the modern era is that
there are so many wonderful, free tools to let machines take care of the
busy-work associated with collaboration, code-hosting, continuous integration,
code quality maintenance, and so on.
There are lots of great
resources
that explain how to automate various things that make maintenance easier.
Here are some things you can configure your Python project to do:
Continuous integration, using any one of a number of providers:
Organize your release notes and versioning with towncrier
All of these tools are wonderful.
But... let’s say you1 maintain a few dozen Python
projects. Being a good maintainer, you’ve started splitting up your big
monolithic packages into smaller ones, so your utility modules can be commonly
shared as widely as possible rather than re-implemented once for each big
frameworks. This is great!
However, every one of those numbered list items above is now a task per
project that you have to repeat from scratch. So imagine a matrix with all
of those down one side and dozens of projects across the top - the full
Cartesian product of these little administrative tasks is a tedious and
exhausting pile of work.
If you’re lucky enough to start every project close to
perfect already, you can
skip some of this work, but that partially just front-loads the tedium; plus,
projects tend to start quite simple, then gradually escalate in complexity, so
it’s helpful to be able to apply these incremental improvements one at a time,
as your project gets bigger.
I really wish there were a tool that could take each of these steps and turn
them into a quick command-line operation; like, I type pyautomate pypi-upload
and the tool notices which CI provider I use, whether I use tox or not, and
adds the appropriate configuration entries to both my CI and tox configuration
to allow me to do that, possibly prompting me for a secret. Same for
pyautomate code-coverage or what have you. All of these automations are
fairly straightforward; almost all of the files you need to edit are easily
parse-able either as yaml, toml, or ConfigParser2 files.
A few years ago, I asked for this to be added to
CookieCutter, but I
think the task is just too big and complicated to reasonably expect the
existing maintainers to ever get around to it.
If you have a bunch of spare time, and really wanted to turbo-charge the Python
open source community, eliminating tons of drag on already-over-committed
maintainers, such a tool would be amazing.
I previously wrote a post about shipping a PyGame app to users on
macOS. It’s now substantially updated
for the new Notarization requirements in Catalina. I hope it’s useful to
somebody!
It’s October, and we’re all getting ready for Halloween, so allow me to me tell you a horror story, in Python:
1
2
>>>0.1+0.2-0.35.551115123125783e-17
Some of you might already be familiar with this chilling tale, but for those who might not have experienced it directly, let me briefly recap.
In Python, the default representation of a number with a decimal point in it is something called an “IEEE 754 double precision binary floating-point number”. This standard achieves a generally useful trade-off between performance, correctness, and is widely implemented in hardware, making it a popular choice for numbers in many programming language.
However, as our spooky story above indicates, it’s not perfect. 0.1 + 0.2 is very slightly less than 0.3 in this representation, because it is a floating-point representation in base 2.
If you’ve worked professionally with software that manipulates money1, you typically learn this lesson early; it’s quite easy to smash head-first into the problem with binary floating-point the first time you have an item that costs 30 cents and for some reason three dimes doesn’t suffice to cover it.
There are a few different approaches to the problem; one is using integers for everything, and denominating your transactions in cents rather than dollars. A strategy which requires less weird unit-conversion2, is to use the built-in decimal module, which provides a floating-point base 10 representation, rather than the standard base-2, which doesn’t have any of these weird glitches surrounding numbers like 0.1.
This is often where a working programmer’s numerical education ends; don’t use floats, they’re bad, use decimals, they’re good. Indeed, this advice will work well up to a pretty high degree of application complexity. But the story doesn’t end there. Once division gets involved, things can still get weird really fast:
The problem is the same: before, we were working with 1/10, a value that doesn’t have a finite (non-repeating) representation in base 2; now we’re working with 1/7, which has the same problem in base 10.
Any time you have a representation of a number which uses digits and a decimal point, no matter the base, you’re going to run in to some rational values which do not have an exact representation with a finite number of digits; thus, you’ll drop some digits off the (necessarily finite) end, and end up with a slightly inaccurate representation.
But Python does have a way to maintain symbolic accuracy for arbitrary rational numbers -- the fractions module!
You can multiply and divide and add and subtract to your heart’s content, and still compare against zero and it’ll always work exactly, giving you the right answers.
So if Python has a “correct” representation, which doesn’t screw up our results under a basic arithmetic operation such as division, why isn’t it the default? We don’t care all that much about performance, right? Python certainly trades off correctness and safety in plenty of other areas.
First of all, while Python’s willing to trade off some storage or CPU efficiency for correctness, precise fractions rapidly consume huge amounts of storage even under very basic algorithms, like consuming gigabytes while just trying to maintain a simple running average over a stream of incoming numbers.
But even more importantly, you’ll notice that I said we could maintain symbolic accuracy for arbitrary rational numbers; but, as it turns out, a whole lot of interesting math you might want to do with a computer involves numbers which are irrational: like π. If you want to use a computer to do it, pretty much all trigonometry3 involves a slightly inaccurate approximation unless you have a literally infinite amount of storage.
As Morpheus put it, “welcome to the desert of the ℝ”.
or any proxy for it, like video-game virtual currency ↩
and less time saying weird words like “nanodollars” to your co-workers ↩
or, for that matter, geometry, or anything involving a square root ↩
I’m a little annoyed at my Apple devices right now.
Time to complain.
“Trust us!” says Apple.
“We’re not like the big, bad Google! We don’t just want to advertise to you
all the time! We’re not like Amazon, just trying to sell you stuff! We care
about your experience. Magical. Revolutionary. Courageous!”
But I can’t hear them over the sound of my freshly-updated Apple TV — the
appliance which exists solely to play Daniel Tiger for our toddler — playing
the John Wick 3 trailer at full volume automatically as soon as it turns on.
For the aforementioned toddler.
I should mention that it is playing this trailer while specifically logged in
to a profile that knows their birth date1 and also their play history2.
I’m aware of the preferences which control autoplay on the home screen; it’s
disabled now. I’m aware that I can put an app other than “TV” in the default
spot, so that I can see ads for other stuff, instead of the stuff “TV” shows me
ads for.
But the whole point of all this video-on-demand junk was supposed to be that I
can watch what I want, when I want — and buying stuff on the iTunes store
included the implicit promise of no advertisements.
At least Google lets me search the web without any full-screen magazine-style
ads popping up.
Launch the app store to check for new versions?
I can’t install my software updates without accidentally seeing HUGE ads for
new apps.
Launch iTunes to play my own music?
I can’t play my own, purchased music without accidentally seeing ads for other
music — and also Apple’s increasingly thirsty, desperate plea for me to
remember that they have a streaming service now. I don’t want it! I know
where Spotify is if I wanted such a thing, the whole reason I’m launching
iTunes is that I want to buy and own the music!
On my iPhone, I can’t even launch the Settings app to turn off my WiFi
without seeing an ad for AppleCare+, right there at the top of the UI, above
everything but my iCloud account. I already have AppleCare+; I bought it
with the phone! Worse, at some point the ad glitched itself out, and now it’s
blank, and when I tap the blank spot where the ad used to be, it just shows me
this:
I just want to use my device, I don’t need ad detritus littering every blank
pixel of screen real estate.
PEP 594 is great news for Python, and in particular for the maintainers of its
standard library, who can now address a reduced surface area. A brief trip
through the PEP’s rogues gallery of modules to deprecate or remove1 is
illuminating. The python standard library contains plenty of useful modules,
but it also hides a veritable necropolis of code, a towering monument to
obsolescence, threatening to topple over on its maintainers at any point.
However, I believe the PEP may be approaching the problem from the wrong
direction. Currently, the standard library is maintained in tandem with, and
by the maintainers of, the CPython python runtime. Large portions of it are
simply included in the hope that it might be useful to somebody. In the
aforementioned PEP, you can see this logic at work in defense of the colorsys
module: why not remove it? “The module is useful to convert CSS colors between
coordinate systems. [It] does not impose maintenance overhead on core
development.”
There was a time when Internet access was scarce, and maybe it was helpful to
pre-load Python with lots of stuff so it could be pre-packaged with the
Python binaries on the
CD-ROM
when you first started learning.
Today, however, the modules you need to convert colors between coordinate
systems are only a pip install away. The bigger core interpreter is just
more to download before you can get started.
Why Didn’t You Review My PR?
So let’s examine that claim: does a tiny module like colorsys “impose
maintenance overhead on core development”?
The core maintainers have enough going on just trying to maintain the huge and
ancient C codebase that is CPython itself. As Mariatta put it in her North
Bay Python
keynote,
the most common question that core developers get is “Why haven’t you looked at
my PR?” And the answer? It’s easier to not look at PRs when you don’t care
about them. This from a talk about what it means to be a core developer!
One might ask, whether Twisted has the same problem. Twisted is a big
collection of loosely-connected modules too; a sort of standard library for
networking. Are clients and servers for SSH, IMAP, HTTP, TLS, et. al. all a
bit much to try to cram into one package?
I’m compelled to reply: yes. Twisted is monolithic because it dates back to
a similar historical period as CPython, where installing stuff was really
complicated. So I am both sympathetic and empathetic towards CPython’s plight.
At some point, each sub-project within Twisted should ideally become a separate
project with its own repository, CI, website, and of course its own more
focused maintainers. We’ve been slowly splitting out projects already, where
we can find a natural boundary. Some things that started in Twisted like
constantly and incremental have been split out; deferred and filepath
are in the process of getting that treatment as well. Other projects absorbed
into the org continue to live separately, like klein and treq. As we
figure out how to reduce the overhead of setting up and maintaining the CI and
release infrastructure for each of them, we’ll do more of this.
But is our monolithic nature the most pressing problem, or even a serious
problem, for the project? Let’s quantify it.
As of this writing, Twisted has 5 outstanding un-reviewed pull requests in our
review queue. The median time a ticket spends in
review is roughly four and a half days.2 The oldest ticket in our queue
dates from April 22, which means it’s been less than 2 months since our oldest
un-reviewed PR was submitted.
It’s always a struggle to find enough maintainers and enough time to respond to
pull requests. Subjectively, it does sometimes feel like “Why won’t you review
my pull request?” is a question we do still get all too often. We aren’t
always doing this well, but all in all, we’re managing; the queue hovers
between 0 at its lowest and 25 or so during a bad month.
By comparison to those numbers, how is core CPython doing?
Looking at CPython’s keyword-based review queue
queue,
we can see that there are 429 tickets currently awaiting review. The oldest
PR awaiting review hasn’t been touched since February 2, 2018, which is almost
500 days old.
How many are interpreter issues and how many are stdlib issues? Clearly review
latency is a problem, but would removing the stdlib even help?
For a quick and highly unscientific estimate, I scanned the first (oldest) page
of PRs in the query above. By my subjective assessment, on this page of 25
PRs, 14 were about the standard library, 10 were about the core language or
interpreter code; one was a minor documentation issue that didn’t really apply
to either. If I can hazard a very rough estimate based on this proportion,
somewhere around half of the unreviewed PRs might be in standard library code.
So the first reason the CPython core team needs to stop maintaining the
standard library because they literally don’t have the capacity to maintain
the standard library. Or to put it differently: they aren’t maintaining it,
and what remains is to admit that and start splitting it out.
It’s true that none of the open PRs on CPython are in colorsys3. It does
not, in fact, impose maintenance overhead on core development. Core
development imposes maintenance overhead on it. If I wanted to update the
colorsys module to be more modern - perhaps to have a Color object rather
than a collection of free functions, perhaps to support integer color models -
I’d likely have to wait 500 days, or more, for a review.
As a result, code in the standard library is harder to change, which means its
users are less motivated to contribute to it. CPython’s unusually infrequent
releases also slow down the development of library code and decrease the
usefulness of feedback from users. It’s no accident that almost all of the
modules in the standard library have actively maintained alternatives outside
of it: it’s not a failure on the part of the stdlib’s maintainers. The whole
process is set up to produce stagnation in all but the most frequently used
parts of the stdlib, and that’s exactly what it does.
New Environments, New Requirements
Perhaps even more importantly is that bundling together CPython with the
definition of the standard library privileges CPython itself, and the use-cases
that it supports, above every other implementation of the language.
Podcast
after
podcast
after
podcast
after keynote tells us that in
order to keep succeeding and expanding, Python needs to grow into new areas:
particularly web frontends, but also mobile clients, embedded systems, and
console games.
a modified, stripped down version of the standard library, which elides most
of it.
In all of these cases, determining which modules have been removed from the
standard library is a sticking point. They have to be discovered by a process
of trial and error; notably, a process completely different from the standard
process for determining dependencies within a Python application. There’s no
install_requires declaration you can put in your setup.py that indicates
that your library uses a stdlib module that your target Python runtime might
leave out due to space constraints.
You can even have this problem even if all you ever use is the standard
python on your Linux installation. Even server- and desktop-class Linux
distributions have the same need for a more minimal core Python package, and so
they already chop up the standard library somewhat arbitrarily. This can break
the expectations of many python codebases, and result in bugs where even pip
install won’t work.
Take It All Out
How about the suggestion that we should do only a little a day? Although it
sounds convincing, don’t be fooled. The reason you never seem to finish is
precisely because you tidy a little at a time. [...] The ultimate secret of
success is this: If you tidy up in one shot, rather than little by little,
you can dramatically change your mind-set.
— Kondō, Marie. “The Life-Changing Magic of Tidying Up” (p. 15-16)
While incremental slimming of the standard library is a step in the right
direction, incremental change can only get us so far. As Marie Kondō says,
when you really want to tidy up, the first step is to take everything out so
that you can really see everything, and put back only what you need.
It’s time to thank those modules which do not spark joy and send them on their
way.
We need a “kernel” version of Python that contains only the most absolutely
minimal library, so that all implementations can agree on a core baseline that
gives you a “python”, and applications, even those that want to run on web
browsers or microcontrollers, can simply state their additional requirements in
terms of requirements.txt.
Now, there are some business environments where adding things to your
requirements.txt is a fraught, bureaucratic process, and in those places, a
large standard library might seem appealing. But “standard library” is a purely
arbitrary boundary that the procurement processes in such places have drawn,
and an equally arbitrary line may be easily drawn around a binary distribution.
So it may indeed be useful for some CPython binary distributions — perhaps even
the official ones — to still ship with a broader selection of modules from
PyPI. Even for the average user, in order to use it for development, at the
very least, you’d need enough stdlib stuff that pip can bootstrap itself, to
install the other modules you need!
It’s already the case, today, that pipis distributed with Python, but
isn’t maintained in the CPython repository. What the default Python binary
installer ships with is already a separate question from what is developed in
the CPython repo, or what ships in the individual source tarball for the
interpreter.
In order to use Linux, you need bootable media with a huge array of additional
programs. That doesn’t mean the Linux kernel itself is in one giant
repository, where the hundreds of applications you need for a functioning Linux
server are all maintained by one team. The Linux kernel project is immensely
valuable, but functioning operating systems which use it are built from the
combination of the Linux kernel and a wide variety of separately maintained
libraries and programs.
Conclusion
The “batteries included” philosophy was a great fit for the time when it was
created: a booster rocket
to sneak Python into the imagination of the programming public. As the open
source and Python packaging ecosystems have matured, however, this strategy has
not aged well, and like any booster, we must let it fall back to earth, lest it
drag us back down with it.
New Python runtimes, new deployment targets, and new developer audiences all
present tremendous opportunities for the Python community to soar ever higher.
But to do it, we need a newer, leaner, unburdened “kernel” Python. We need to
dump the whole standard library out on the floor, adding back only the smallest
bits that we need, so that we can tell what is truly necessary and what’s just
nice to have.
I hope I’ve convinced at least a few of you that we need a kernel Python.
Thanks to Jean-Paul Calderone, Donald Stufft, Alex Gaynor, Amber Brown, Ian
Cordasco, Jonathan Lange, Augie Fackler, Hynek Schlawack, Pete Fein, Mark
Williams, Tom Most, Jeremy Thurgood, and Aaron Gallagher for feedback and
corrections on earlier drafts of this post. Any errors of course remain my
own.
sunau, xdrlib, and chunk are my personal favorites. ↩