Planet Twisted

February 04, 2023

Thomas Vander Stichele

How Not to Say the Wrong Thing

"if you’re going to open your mouth, ask yourself if what you are about to say is likely to provide comfort and support. If it isn’t, don’t say it. Don’t, for example, give advice."

Susan Silk's Ring Theory is a helpful model to navigate what not to say during times of grief and traumatic events.

Picture a center ring, and inside it the people most affected by what's going on. Picture a larger circle around it, with inside it the people closest to those in the center. Repeat outwards.

The person in the center ring can say anything they want to anyone, anywhere.
Everyone else can say those things too, but only to people in the larger outside rings. Otherwise, you support and comfort.

Now, consider where in this diagram you are, and where the people you are talking to are.

"Comfort IN, dump OUT."

This model applies in other situations - for example, managers are better off complaining to their own managers or peers, while supporting their own reports and absorbing their complaints with empathy and compassion.

Taken from The Playlist - a curated perspective on the intersection of form and content (subscribe, discuss)

flattr this!

by Thomas at February 04, 2023 02:27 AM

SRE Philosophy With Jennifer Mace

"Even the most junior SRE on call starts having director authority. [..] There is a power in that relationship that SRE does have when they think something is in danger. And it's a power we have to be careful not to misuse. But it's important, because that's our job."

Macey is the guest on Episode 1 of SRE Prodcast, Google's podcast about Site Reliability Engineering. She goes in-depth on some of the core tenets of SRE, including risk, on-call, toil, design involvement, and more. (As a side note, I'm reasonably certain that I'm not the entertaining Belgian that was causing her team failure loops, but I'm too afraid to ask.)

The whole series is worth a listen, but just like the podcast itself - start with macey's advice.

"My definition of toil: toil is boring or repetitive work that does not gain you a permanent improvement."

Taken from The Playlist - a curated perspective on the intersection of form and content (subscribe, discuss)

flattr this!

by Thomas at February 04, 2023 02:27 AM

January 23, 2023

Glyph Lefkowitz

A Very Silly Program

One of the persistently lesser-known symptoms of ADHD is hyperfocus. It is sometimes quasi-accurately described as a “superpower”1 2, which it can be. In the right conditions, hyperfocus is the ability to effortlessly maintain a singular locus of attention for far longer than a neurotypical person would be able to.

However, as a general rule, it would be more accurate to characterize hyperfocus not as an “ability to focus on X” but rather as “an inability to focus on anything other than X”. Sometimes hyperfocus comes on and it just digs its claws into you and won’t let go until you can achieve some kind of closure.

Recently, the X I could absolutely not stop focusing on — for days at a time — was this extremely annoying picture:

chroma subsampling carnage

Which lead to me writing the silliest computer program I have written in quite some time.


You see, for some reason, macOS seems to prefer YUV422 chroma subsampling3 on external displays, even when the bitrate of the connection and selected refresh rate support RGB.4 Lots of people have been trying to address this for a literal decade5 6 7 8 9 10 11, and the problem has gotten worse with Apple Silicon, where the operating system no longer even supports the EDID-override functionality available on every other PC operating system that supports plugging in a monitor.

In brief, this means that every time I unplug my MacBook from its dock and plug it back in more than 5 minutes later, its color accuracy is destroyed and red or blue text on certain backgrounds looks like that mangled mess in the picture above. Worse, while the color distinction is definitely noticeable, it’s so subtle that it’s like my display is constantly gaslighting me. I can almost hear it taunting me:

Magenta? Yeah, magenta always looked like this. Maybe it’s the ambient lighting in this room. You don’t even have a monitor hood. Remember how you had to use one of those for print design validation? Why would you expect it to always look the same without one?

Still, I’m one of the luckier people with this problem, because I can seem to force RGB / 444 color format on my display just by leaving the display at 120Hz rather than 144, then toggling HDR on and then off again. At least I don’t need to plug in the display via multiple HDMI and displayport cables and go into the OSD every time. However, there is no API to adjust, or even discover the chroma format of your connected display’s link, and even the accessibility features that supposedly let you drive GUIs are broken in the system settings “Displays” panel12, so you have to do it by sending synthetic keystrokes and hoping you can tab-focus your way to the right place.

Anyway, this is a program which will be useless to anyone else as-is, but if someone else is struggling with the absolute inability to stop fiddling with the OS to try and get colors to look correct on a particular external display, by default, all the time, maybe you could do something to hack on this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import os
from Quartz import CGDisplayRegisterReconfigurationCallback, kCGDisplaySetMainFlag, kCGDisplayBeginConfigurationFlag
from ColorSync import CGDisplayCreateUUIDFromDisplayID
from CoreFoundation import CFUUIDCreateString
from AppKit import NSApplicationMain, NSApplicationActivationPolicyAccessory, NSApplication

NSApplication.sharedApplication().setActivationPolicy_(NSApplicationActivationPolicyAccessory)

CGDirectDisplayID = int
CGDisplayChangeSummaryFlags = int

MY_EXTERNAL_ULTRAWIDE = '48CEABD9-3824-4674-9269-60D1696F0916'
MY_INTERNAL_DISPLAY = '37D8832A-2D66-02CA-B9F7-8F30A301B230'

def cb(display: CGDirectDisplayID, flags: CGDisplayChangeSummaryFlags, userInfo: object) -> None:
    if flags & kCGDisplayBeginConfigurationFlag:
        return
    if flags & kCGDisplaySetMainFlag:
        displayUuid = CGDisplayCreateUUIDFromDisplayID(display)
        uuidString = CFUUIDCreateString(None, displayUuid)
        print(uuidString, "became the main display")
        if uuidString == MY_EXTERNAL_ULTRAWIDE:
            print("toggling HDR to attempt to clean up subsampling")
            os.system("/Users/glyph/.local/bin/desubsample")
            print("HDR toggled.")

print("registered", CGDisplayRegisterReconfigurationCallback(cb, None))

NSApplicationMain([])

and the linked desubsample is this atrocity, which I substantially cribbed from this helpful example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#!/usr/bin/osascript

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use framework "AppKit"
use scripting additions

tell application "System Settings"
    quit
    delay 1
    activate
    current application's NSWorkspace's sharedWorkspace()'s openURL:(current application's NSURL's URLWithString:"x-apple.systempreferences:com.apple.Displays-Settings.extension")
    delay 0.5

    tell application "System Events"
    tell process "System Settings"
        key code 48
        key code 48
        key code 48
            delay 0.5
        key code 49
        delay 0.5
        -- activate hdr on left monitor

        set hdr to checkbox 1 of group 3 of scroll area 2 of ¬
                group 1 of group 2 of splitter group 1 of group 1 of ¬
                window "Displays"
        tell hdr
                click it
                delay 1.0
                if value is 1
                    click it
                end if
        end tell

    end tell
    end tell
    quit
end tell

This ridiculous little pair of programs does it automatically, so whenever I reconnect my MacBook to my desktop dock at home, it faffs around with clicking the HDR button for me every time. I am leaving it running in a background tmux session so — hopefully — I can finally stop thinking about this.

by Glyph at January 23, 2023 03:06 AM

January 18, 2023

Hynek Schlawack

Why I Like Nox

Ever since I got involved with open-source Python projects, tox has been vital for testing packages across Python versions (and other factors). However, lately, I’ve been increasingly using Nox for my projects instead. Since I’ve been asked why repeatedly, I’ll sum up my thoughts.

by Hynek Schlawack (hs@ox.cx) at January 18, 2023 12:00 PM

January 09, 2023

Hynek Schlawack

December 31, 2022

Moshe Zadka

The "Dynamic" Properties in PyProject

When writing a pyproject.toml file, the project section is optional. However, if it does exist, two of its properties are required:

  • name
  • version

If these two properties are not there, the section will be ignored.

This is a lie. But it is not a big lie: it is almost true.

In general, if either of these two properties are not there, the section will be ignored. However, there is a way to indicate that either, or both, of these properties will be filled in by the build system later on.

This is done with dynamic.

For example

[project]
name = "my-package"
dynamic = ["version"]

This is the most common setting. However, it is possible to set dynamic to ["name", "version"] and avoid both parameters.

by Moshe Zadka at December 31, 2022 04:00 AM

December 12, 2022

Glyph Lefkowitz

Potato Programming

One potato, two potato, three potato, four
Five potato, six potato, seven potato, more.

Traditional Children’s Counting Rhyme

Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%.

Knuth, Donald
“Structured Programming with go to statements”
Computing Surveys, Vol. 6, No. 4, December 1974
(p. 268)
(Emphasis mine)

Knuth’s admonition about premature optimization is such a cliché among software developers at this point that even the correction to include the full context of the quote is itself a a cliché.

Still, it’s a cliché for a reason: the speed at which software can be written is in tension — if not necessarily in conflict — with the speed at which it executes. As Nelson Elhage has explained, software can be qualitatively worse when it is slow, but spending time optimizing an algorithm before getting any feedback from users or profiling the system as a whole can lead one down many blind alleys of wasted effort.

In that same essay, Nelson further elaborates that performant foundations simplify architecture1. He then follows up with several bits of architectural advice that is highly specific to parsing—compilers and type-checkers specifically—which, while good, is hard to generalize beyond “optimizing performance early can also be good”.

So, here I will endeavor to generalize that advice. How does one provide a performant architectural foundation without necessarily wasting a lot of time on early micro-optimization?

Enter The Potato

Many years before Nelson wrote his excellent aforementioned essay, my father coined a related term: “Potato Programming”.

In modern vernacular, a potato is very slow hardware, and “potato programming” is the software equivalent of the same.

The term comes from the rhyme that opened this essay, and is meant to evoke a slow, childlike counting of individual elements as an algorithm operates upon them. it is an unfortunately quite common software-architectural idiom whereby interfaces are provided in terms of scalar values. In other words, APIs that require you to use for loops or other forms of explicit, individual, non-parallelized iteration. But this is all very abstract; an example might help.

For a generic business-logic example, let’s consider the problem of monthly recurring billing. Every month, we pull in the list of all of all subscriptions to our service, and we bill them.

Since our hypothetical company has an account-management team that owns the UI which updates subscriptions and a billing backend team that writes code to interface with 3rd-party payment providers, we’ll create 2 backends, here represented by some Protocols.

Finally, we’ll have an orchestration layer that puts them together to actually run the billing. I will use async to indicate which things require a network round trip:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class SubscriptionService(Protocol):
    async def all_subscriptions(self) -> AsyncIterable[Subscription]:
        ...

class Subscription(Protocol):
    account_id: str
    to_charge_per_month: money

class BillingService(Protocol):
    async def bill_amount(self, account_id: str, amount: money) -> None:
        ...

To many readers, this may look like an entirely reasonable interface specification; indeed, it looks like a lot of real, public-facing “REST” APIs. An equally apparently-reasonable implementation of our orchestration between them might look like this:

1
2
3
async def billing(s: SubscriptionService, b: BillingService) -> None:
    async for sub in s.all_subscriptions():
        await b.bill_amount(sub.account_id, sub.to_charge_per_month)

This is, however, just about the slowest implementation of this functionality that it’s possible to implement. So, this is the bad version. Let’s talk about the good version: no-tato programming, if you will. But first, some backstory.

Some Backstory

My father began his career as an APL programmer, and one of the key insights he took away from APL’s architecture is that, as he puts it:

Computers like to do things over and over again. They like to do things on arrays. They don’t want to do things on scalars. So, in fact, it’s not possible to write a program that only does things on a scalar. [...] You can’t have an ‘integer’ in APL, you can only have an ‘array of integers’. There’s no ‘loop’s, there’s no ‘map’s.

APL, like Python2, is typically executed via an interpreter. Which means, like Python, execution of basic operations like calling functions can be quite slow. However, unlike Python, its pervasive reliance upon arrays meant that almost all of its operations could be safely parallelized, and would only get more and more efficient as more and more parallel hardware was developed.

I said ‘unlike Python’ there, but in fact, my father first related this concept to me regarding a part of the Python ecosystem which follows APL’s design idiom: NumPy. NumPy takes a similar approach: it cannot itself do anything to speed up Python’s fundamental interpreted execution speed3, but it can move the intensive numerical operations that it implements into operations on arrays, rather than operations on individual objects, whether numbers or not.

The performance difference involved in these two styles is not small. Consider this case study which shows a 5828% improvement4 when taking an algorithm from idiomatic pure Python to NumPy.

This idiom is also more or less how GPU programming works. GPUs cannot operate on individual values. You submit a program5 to the GPU, as well as a large array of data6, and the GPU executes the program on that data in parallel across hundreds of tiny cores. Submitting individual values for the GPU to work on would actually be much slower than just doing the work on the CPU directly, due to the bus latency involved to transfer the data back and forth.

Back from the Backstory

This is all interesting for a class of numerical software — and indeeed it works very well there — but it may seem a bit abstract to web backend developers just trying to glue together some internal microservice APIs, or indeed most app developers who aren’t working in those specialized fields. It’s not like Stripe is going to let you run their payment service on your GPU.

However, the lesson generalizes quite well: anywhere you see an API defined in terms of one-potato, two-potato iteration, ask yourself: “how can this be turned into an array”? Let’s go back to our example.

The simplest change that we can make, as a consumer of these potato-shaped APIs, is to submit them in parallel. So if we have to do the optimization in the orchestration layer, we might get something more like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from asyncio import Semaphore, AbstractEventLoop

async def one_bill(
    loop: AbstractEventLoop,
    sem: Semaphore,
    sub: Subscription,
    b: BillingService,
) -> None:
    await sem.acquire()
    async def work() -> None:
        try:
            await b.bill_amount(sub.account_id, sub.to_charge_per_month)
        finally:
            sem.release()
    loop.create_task(work)

async def billing(
    loop: AbstractEventLoop,
    s: SubscriptionService,
    b: BillingService,
    batch_size: int,
) -> None:
    sem = Semaphore(batch_size)
    async for sub in s.all_subscriptions():
        await one_bill(loop, sem, sub, b)

This is an improvement, but it’s a bit of a brute-force solution; a multipotato, if you will. We’ve moved the work to the billing service faster, but it still has to do just as much work. Maybe even more work, because now it’s potentially got a lot more lock-contention on its end. And we’re still waiting for the Subscription objects to dribble out of the SubscriptionService potentially one request/response at a time.

In other words, we have used network concurrency as a hack to simulate a performant design. But the back end that we have been given here is not actually optimizable; we do not have a performant foundation. As you can see, we have even had to change our local architecture a little bit here, to include a loop parameter and a batch_size which we had not previously contemplated.

A better-designed interface in the first place would look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class SubscriptionService(Protocol):
    async def all_subscriptions(
        self, batch_size: int,
    ) -> AsyncIterable[Sequence[Subscription]]:
        ...

class Subscription(Protocol):
    account_id: str
    to_charge_per_month: money

@dataclass
class BillingRequest:
    account_id: str
    amount: money

class BillingService(Protocol):
    async def submit_bills(
        self,
        bills: Sequence[BillingRequest],
    ) -> None:
        ...

Superficially, the implementation here looks slightly more awkward than our naive first attempt:

1
2
3
4
5
6
7
8
async def billing(s: SubscriptionService, b: BillingService, batch_size: int) -> None:
    async for sub_batch in s.all_subscriptions(batch_size):
        await b.submit_bills(
            [
                BillingRequest(sub.account_id, sub.to_charge_per_month)
                for sub in subs
            ]
        )

However, while the implementation with batching in the backend is approximately as performant as our parallel orchestration implementation, backend batching has a number of advantages over parallel orchestration.

First, backend batching has less internal complexity; no need to have a Semaphore in the orchestration layer, or to create tasks on an event loop. There’s less surface area here for bugs.

Second, and more importantly: backend batching permits for future optimizations within the backend services, which are much closer to the relevant data and can achieve more substantial gains than we can as a client without knowledge of their implementation.

There are many ways this might manifest, but consider that each of these services has their own database, and have got to submit queries and execute transactions on those databases.

In the subscription service, it’s faster to run a single SELECT statement that returns a bunch of results than to select a single result at a time. On the billing service’s end, it’s much faster to issue a single INSERT or UPDATE and then COMMIT for N records at once than to concurrently issue a ton of potentially related modifications in separate transactions.

Potato No Mo

The initial implementation within each of these backends can be as naive and slow as necessary to achieve an MVP. You can do a SELECT … LIMIT 1 internally, if that’s easier, and performance is not important at first. There can be a mountain of potatoes hidden behind the veil of that batched list. In this way, you can avoid the potential trap of premature optimization. Maybe this is a terrible factoring of services for your application in the first place; best to have that prototype in place and functioning quickly so that you can throw it out faster!

However, by initially designing an interface based on lists of things rather than individual things, it’s much easier to hide irrelevant implementation details from the client, and to achieve meaningful improvements when optimizing.

Acknowledgements

This is the first post supported by my Patreon, with a topic suggested by a patron.


  1. It’s a really good essay, you should read it. 

  2. Yes, I know it’s actually bytecode compiled and then run on a custom interpreting VM, but for the purposes of comparing these performance characteristics “interpreted” is a more accurate approximation. Don’t @ me. 

  3. Although, thankfully, a lot of folks are now working very hard on that problem. 

  4. No, not a typo, that’s a 4-digit improvement. 

  5. Typically called a “shader” due to its origins in graphically shading polygons. 

  6. The data may rerepresenting vertices in a 3-D mesh, pixels in texture data, or, in the case of general-purpose GPU programming, “just a bunch of floating-point numbers”. 

by Glyph at December 12, 2022 09:40 PM

October 27, 2022

Glyph Lefkowitz

Super Swing Districts

In my corner of the social graph, when we talk about politics today, we tend to use a lot of moralizing language. A lot of emotive language. And that makes sense; overt fascist are repeating the strategy of using the right of trans people to, like, be alive, as a wedge issue to escalate to full-blown eugenics and antisemitism. There’s a lot of moral stuff and a lot of emotional stuff happening there.

But when we get down to it, politics is a highly technical discipline that requires a lot of work. You don’t need to just have the right opinion, you have to actually do a lot of math to figure out efficient ways to deploy resources, effective strategies to convince the undecided and to command the attention of the disengaged. It’s also adversarial: the bad guys are trying to do the same thing, so if you do find some efficient way to campaign, they will soon find out and try to dismantle it.

So while we might talk abstractly about “doing the work”, a lot of the work is tedious and difficult analysis of a lot of very confusing numbers. Not to mention the fact that it requires maintaining the tenacious mindset of a happy Sisyphus due to its adversarial nature. To be frank, I’m not great at either of those things.

Luckily, my uncle is. He is a professor of political science who — beyond the obvious familial bias I might have — I tend to think is a really smart guy with a lot of good ideas. More importantly, however, is that he does do “the work” I’m talking about here.

So here is some of that work: SuperSwingDistricts.org. This is a slate of democratic downballot candidates for office across the USA who need your support right now. Specifically it is a carefully curated slate to maximize spend efficiency via the reverse-coattails effect, multiplied by finding the areas where there are the most overlapping high-leverage elections. You can read more about the specifics on the website, and the specifics of the vetting of the candidates bona-fides, but you can also just take my word for it and Donate Now via ActBlue. Just like... gobs of money.

Political fundraising is not really my wheelhouse, and I am not that comfortable doing it. I hope that we can stop this “democracy” machine from constantly falling apart all the time so I can work on fixing the other broken systems in my life like Python-langauge native application packaging for various platforms. But this one is really, really important. Many of these candidates are in pivotal positions that will help prevent authoritarians from seizing the actual physical mechanisms of elections themselves, and attempting a more successful coup in 2024.

So: donate now.

by Glyph at October 27, 2022 06:49 AM

October 14, 2022

Hynek Schlawack

How to Fix the set-output GitHub Actions Deprecation Warning

If you have a GitHub Actions workflow that sets an output using echo ::set-output key=value, you have started to see an unhelpful deprecation warning. Here’s how to fix it.

by Hynek Schlawack (hs@ox.cx) at October 14, 2022 12:00 AM

September 19, 2022

Hynek Schlawack

September 07, 2022

Hynek Schlawack

You Can Build Portable Binaries of Python Applications

Contrary to popular belief, it’s possible to ship portable executables of Python applications without sending your users to Python packaging hell.

by Hynek Schlawack (hs@ox.cx) at September 07, 2022 12:00 AM

August 18, 2022

Hynek Schlawack

Easier Crediting of Contributors on GitHub

GitHub has the concept of co-authors of a commit. You’ve probably seen it in the web UI, when multiple people are listed to have committed something. I want to be gracious with credit where it’s due and I’ve found ways to make it easier.

by Hynek Schlawack (hs@ox.cx) at August 18, 2022 12:00 AM

August 14, 2022

Moshe Zadka

On The Go

Now that travel is more realistic, I have started to optimize how well I can work on the go. I want to be able to carry as few things as possible, and have the best set-up possible.

Charging

Power Bank charging

The "center" of the mobile set-up is my Anker Power Bank. It serves two purposes:

  • It is my wall-plug charger.
  • It is my "mobile power": I can carry around 10k mAH of energy.

The charger has two USB-C slots and one USB-A slot.

Compute

M1 MacBook Air with stickers

For "compute", I have three devices:

  • M1 MacBook Air
  • Galaxy Samsung S9+ (I know it's a bit old)
  • FitBit Charge 4

The S9 is old enough that there is no case with a MagSafe compatible back. Instead, I got a MagSafe sticker that goes on the back of the case.

This allowed me to get a MagSafe Pop-Socket base. Sticking a Pop-Socket on top of it lets me hold the phone securely, and avoids it falling on my face at night.

Ear buds

For earbuds, I have the TOZO T10. They come in multiple colors!

The colors are not just an aesthetic choice. They also serve a purpose: I have a black one and a khaki one.

The black one is paired to my phone. The khaki one is paired to my laptop.

I can charge the TOZO cases with either the USB-C cable or the PowerWave charger, whichever is free.

Charging

Phone charging with a wireless MagSafe charger

In order to charge the M1 I have a USB-C "outtie"/USB-C "outtie" 3 foot wire. It's a bit short, but this also means it takes less space. The FitBit Charge comes with its own USB-A custom cable.

For wireless charging, I have the Anker PowerWave. It's MagSafe compatible, and can connect to any USB-C-compatible outlet.

The phone is only charged by the wireless charging. The USB-C input is wonky, and can be incompatible with humid climates.

I connected a Pop Socket to the back of the PowerWave charger. This means that while the phone is charging, I can still hold it securely.

Together, they give me a "wireless charging" battery. The PowerWave connects to the phone, and the Power Bank has plenty of energy to last for a while while not connecting to anything.

I cannot charge all devices at once. But I can charge all devices, and (almost) any three at once.

Hub

USB-C hub

The last device I have is an older version of the Anker 5-in-1 hub. This allows connecting USB Drives and HDMI connectors.

Case

Power Bank charging

All of these things are carried in a Targus TSS912 case. The laptop goes inside the sleeve, while the other things all go in the side pocket.

The side pocket is small, but can fit all of the things above. Because of its size, it does get crowded. In order to find things easily, I keep all of these things in separate sub-pockets.

I keep the Power Bank, the MagSafe charger, and the USB-C/USB-C cable in the little pouch that comes with the Power Bank.

The hub and FitBit charging cable go into a ziplock bag. Those things see less use.

The earbud cases go into the pocket as-is. They are easy enough to dig out by rooting around.

I wanted a messenger-style case so that I can carry it while I have a backpack on. Whether I am carrying my work laptop (in the work backpack) or a travel backpack, this is a distinct advantage.

The case is small enough to be slipped inside another backpack. If I am carrying a backpack, and there's enough room, I can consolidate.

Conclusion

I chose this set up for options.

For example, if my phone is low on battery, I can connect the PowerWave to the bank, leave the bank in the side-bag's pocket, and and keep using the phone while it is charging, holding it with the PowerWave's pop-sockets.

If I am listening to a podcast while walking around, and notice that the ear bud's case is low on battery, I can connect the case to the bank while they are both in the side-bag's pocket.

When sitting down at a coffee shop or an office, I can connect the bank to the wall socket and charge any of my devices while sitting there. As a perk the bank is charging while I'm sitting down.

by Moshe Zadka at August 14, 2022 09:00 PM

August 12, 2022

Hynek Schlawack

July 29, 2022

Hynek Schlawack

Recursive Optional Dependencies in Python

One of my (slowly evaporating) reasons why I like putting packaging metadata into an executable setup.py is the ability to have optional dependencies that are combinations of others. As of pip 21.2, this is possible without running code.

by Hynek Schlawack (hs@ox.cx) at July 29, 2022 06:00 AM

June 21, 2022

Hynek Schlawack

“Don’t Mock What You Don’t Own” in 5 Minutes

A common issue when writing tests for real-world software is how to deal with third-party dependencies. Let’s examine an old, but counter-intuitive principle.

by Hynek Schlawack (hs@ox.cx) at June 21, 2022 09:00 AM

June 06, 2022

Glyph Lefkowitz

Dates And Times And Types

Python’s standard datetime module is very powerful. However, it has a couple of annoying flaws.

Firstly, datetimes are considered a kind of date1, which causes problems. Although datetime is a literal subclass of date so Mypy and isinstance believe a datetime “is” a date, you cannot substitute a datetime for a date in a program without provoking errors at runtime.

To put it more precisely, here are two programs which define a function with type annotations, that mypy finds no issues with. The first of which even takes care to type-check its arguments at run-time. But both raise TypeErrors at runtime:

Comparing datetime to date:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from datetime import date, datetime

def is_after(before: date, after: date) -> bool | None:
    if not isinstance(before, date):
        raise TypeError(f"{before} isn't a date")
    if not isinstance(after, date):
        raise TypeError(f"{after} isn't a date")
    if before == after:
        return None
    if before > after:
        return False
    return True

is_after(date.today(), datetime.now())
1
2
3
4
5
6
Traceback (most recent call last):
  File ".../date_datetime_compare.py", line 14, in <module>
    is_after(date.today(), datetime.now())
  File ".../date_datetime_compare.py", line 10, in is_after
    if before > after:
TypeError: can't compare datetime.datetime to datetime.date

Comparing “naive” and “aware” datetime:

1
2
3
4
5
6
from datetime import datetime, timezone, timedelta

def compare(a: datetime, b: datetime) -> timedelta:
    return a - b

compare(datetime.now(), datetime.now(timezone.utc))
1
2
3
4
5
6
Traceback (most recent call last):
  File ".../naive_aware_compare.py", line 6, in <module>
    compare(datetime.now(), datetime.now(timezone.utc))
  File ".../naive_aware_compare.py", line 4, in compare
    return a - b
TypeError: can't subtract offset-naive and offset-aware datetimes

In some sense, the whole point of using Mypy - or, indeed, of runtime isinstance checks - is to avoid TypeError getting raised. You specify all the types, the type-checker yells at you, you fix it, and then you can know your code is not going to blow up in unexpected ways.

Of course, it’s still possible to avoid these TypeErrors with runtime checks, but it’s tedious and annoying to need to put a check for .tzinfo is not None or not isinstance(..., datetime) before every use of - or >.

The problem here is that datetime is trying to represent too many things with too few types. datetime should not be inheriting from date, because it isn’t a date, which is why > raises an exception when you compare the two.

Naive datetimes represent an abstract representation of a hypothetical civil time which are not necessarily tethered to specific moments in physical time. You can’t know exactly what time “today at 2:30 AM” is, unless you know where on earth you are and what the rules are for daylight savings time in that place. However, you can still talk about “2:30 AM” without reference to a time zone, and you can even say that “3:30 AM” is “60 minutes after” that time, even if, given potential changes to wall clock time, that may not be strictly true in one specific place during a DST transition. Indeed, one of those times may refer to multiple points in civil time at a particular location, when attached to different sides of a DST boundary.

By contrast, Aware datetimes represent actual moments in time, as they combine civil time with a timezone that has a defined UTC offset to interpret them in.

These are very similar types of objects, but they are not in fact the same, given that all of their operators have slightly different (albeit closely related) semantics.

Using datetype

I created a small library, datetype, which is (almost) entirely type-time behavior. At runtime, despite appearances, there are no instances of new types, not even wrappers. Concretely, everything is a date, time, or datetime from the standard library. However, when type-checking with Mypy, you will now get errors reported from the above scenarios if you use the types from datetype.

Consider this example, quite similar to our first problematic example:

Comparing AwareDateTime or NaiveDateTime to date:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from datetype import Date, NaiveDateTime

def is_after(before: Date, after: Date) -> bool | None:
    if before == after:
        return None
    if before > after:
        return False
    return True

is_after(Date.today(), NaiveDateTime.now())

Now, instead of type-checking cleanly, it produces this error, letting you know that this call to is_after will give you a TypeError.

1
2
date_datetime_datetype.py:10: error: Argument 2 to "is_after" has incompatible type "NaiveDateTime"; expected "Date"
Found 1 error in 1 file (checked 1 source file)

Similarly, attempting to compare naive and aware objects results in errors now. We can even use the included AnyDateTime type variable to include a bound similar to AnyStr from the standard library to make functions that can take either aware or naive datetimes, as long as you don’t mix them up:

Comparing AwareDateTime to NaiveDateTime:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from datetime import datetime, timezone, timedelta
from datetype import AwareDateTime, NaiveDateTime, AnyDateTime


def compare_same(a: AnyDateTime, b: AnyDateTime) -> timedelta:
    return a - b


def compare_either(
    a: AwareDateTime | NaiveDateTime,
    b: AwareDateTime | NaiveDateTime,
) -> timedelta:
    return a - b


compare_same(NaiveDateTime.now(), AwareDateTime.now(timezone.utc))

compare_same(AwareDateTime.now(timezone.utc), AwareDateTime.now(timezone.utc))
compare_same(NaiveDateTime.now(), NaiveDateTime.now())
1
2
3
4
5
6
naive_aware_datetype.py:13: error: No overload variant of "__sub__" of "_GenericDateTime" matches argument type "NaiveDateTime"
...
naive_aware_datetype.py:13: error: No overload variant of "__sub__" of "_GenericDateTime" matches argument type "AwareDateTime"
...
naive_aware_datetype.py:16: error: Value of type variable "AnyDateTime" of "compare_same" cannot be "_GenericDateTime[Optional[tzinfo]]"
Found 3 errors in 1 file (checked 1 source file)

Telling the Difference

Although the types in datetype are Protocols, there’s a bit of included magic so that you can use them as type guards with isinstance like regular types. For example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from datetype import NaiveDateTime, AwareDateTime
from datetime import datetime, timezone

nnow = NaiveDateTime.now()
anow = AwareDateTime.now(timezone.utc)


def check(d: AwareDateTime | NaiveDateTime) -> None:
    if isinstance(d, NaiveDateTime):
        print("Naive!", d - nnow)
    elif isinstance(d, AwareDateTime):
        print("Aware!", d - anow)


check(NaiveDateTime.now())
check(AwareDateTime.now(timezone.utc))

Try it out, carefully

This library is very much alpha-quality; in the process of writing this blog post, I made a half a dozen backwards-incompatible changes, and there are still probably a few more left as I get feedback. But if this is a problem you’ve had within your own codebases - ensuring that dates and datetimes don’t get mixed up, or requiring that all datetimes crossing some API boundary are definitely aware and not naive, give it a try with pip install datetype and let me know if it catches any bugs!


  1. But, in typical fashion, not a kind of time... 

by Glyph at June 06, 2022 07:27 AM

May 17, 2022

Hynek Schlawack

Better Python Object Serialization

The Python standard library is full of underappreciated gems. One of them allows for simple and elegant function dispatching based on argument types. This makes it perfect for serialization of arbitrary objects – for example to JSON in web APIs and structured logs.

by Hynek Schlawack (hs@ox.cx) at May 17, 2022 12:00 AM

May 12, 2022

Glyph Lefkowitz

Leave The Frog For Last

This was originally a thread on Twitter; you can read the original here, but this one has been lightly edited for grammar and clarity, plus I added a pretty rad picture of a frog to it.

Update 2022-05-16: Thanks to some reader feedback I have updated the conclusion to note an example where this advice can productively apply to some ADHDers.

I’m in the midst of trying to unlearn a few things about neurotypical productivity advice but this is one I’ve been thinking about a lot:

“Eat the frog first” is particularly toxic advice for ADHDers.

A frog on a flower, nervously looking at you as you contemplate whether to eat it.

Photo by Stephanie LeBlanc on Unsplash

First, for anyone who happens not to know already: “eat the frog first” is a technique which involves finding the task you’re most likely to ignore or put off, and doing it first in your day to ensure that you don’t avoid it.

For a neurotypical person, eating the frog first makes sense, which is of course why this advice exists in the first place. If you’ve been avoiding a task, put it first in your day when you’re going to have the most energy, and use the allure of the more fun tasks later to push through it.

This makes intuitive sense.

The premise of this advice is that you rely on the promise of delayed gratification—and the anticipated inherent satisfaction of having completed the boring and unpleasant thing—in order to motivate you to do it.

Here’s the problem for ADHDers: ADHD is literally the condition of not generating enough dopamine, which means delayed gratification is inherently more difficult for us. The anticipated inherent satisfaction is less motivating because it’s less intense, on a physical level.

An ADHD brain powering through tasks needs momentum. You need to be in a sufficiently excited state to begin doing things. A bored, dopamine-starved ADHD brain is going to be clawing at the walls looking for ANY dopamine-generating distraction to avoid thinking about the frog.

Of course where dopamine won’t do, there’s always adrenaline. Panic can trigger sufficient states of activity as well, although the stress is unhealthy and it’s less reliable in the absence of a real, immediate threats that you can’t ignore.

So what frog-first ADHD days often look like (particularly for adult ADHDers) is a slow slog of not really doing anything useful, while stewing in increasingly negative self-talk, attempting to generate the necessary anger and self-loathing required to truly panic about the frog.

Unfortunately this type of attempt at internal motivation is more likely to result in depression than motivation, which creates a spiral that makes the problem worse.

The neurotypical’s metaphorical frog is just sitting there, waiting to be eaten. Maybe they’ve been avoiding it because it’s a little gross, but fine, they can exert a little willpower and just do it, and move on to more pleasant activities. But the ADHD frog is running away.

Trying to use the same technique just results in the ADHDer sitting in the swamp where the frog used to be, chugging ever-increasing volumes of toxic mud in the hopes that we’ll find a frog in there. Sometimes we even find one! But that’s not success.

At the end of the day, the metaphorical frog does need eating; that’s what makes it a frog. What is the conscientious ADHDer to do?

Unfortunately, there is no singular, snappy answer; difficulty with this type of task is the impenetrable core of the “disorder” part of ADHD. It’ll always be difficult. But there are definitely strategies which can make it relatively easier.

None of these are guaranteed to work, but I am at least reasonably sure that they won’t build a spiral into guilt and depression:

  1. start with a fun task, and build momentum until the frog seems like no big deal
  2. use hype music; yell; get excited to an embarrassing degree.1
  3. exercise; i.e. “go for a walk”

It might literally be better to start the day with something actively unproductive, but fun, like a video game, although this can obviously be risky. For this to work, you need to have very good systems in place.

Start the frog at the end of the day and deliberately interrupt yourself when you stop work. Leave it lingering so some aspect of it annoys you and it distracts you at night. Start the next day pissed off at and obsessing over murdering that piece of shit frog as soon as you can get your hands on it.

This technique is also good because at the end of the day you only need to push yourself just hard enough to load the task into your brain, not all the way through it.

Remember that while “stimulated” doesn’t have to mean “panicked”, it also doesn’t need to mean “happy”. Sometimes, annoyance or irritation is the best way to ensure that you go do something. Consider, for example, the compelling motivation of reading a comment on the Internet that you disagree with.

Overall the distinguishing characteristic of toxic productivity advice is that it makes you spend more time feeling bad than doing stuff. It substitutes panic for healthy motivation, and low self-esteem for a feeling of accomplishment.

The most important point I am trying to make is this: when you take productivity advice — even, or perhaps especially, from me – try to measure its impact on your work and your mental health.

To that point, one piece of feedback I received on an earlier iteration of this article was that, for some ADHDers on stimulant medication2, eating the frog first can work: if you take your medication early in the morning and experience a big, but temporary, increase to executive-function 30 minutes later, being prepared to do your frog-eating at that specific moment can have similar results as for someone more neurotypical. This very much depends on how you specifically react to your medication, however.

So, if eating the frog first is working for you, by all means keep doing it, but you have to ask yourself: are you actually getting more done?


  1. One of the advantages of working from home is that you can really lean into this without provoking an intervention from your coworkers. 

  2. I personally take a slightly unusual kind of ADHD medication, which does help but not in the typical fashion. 

by Glyph at May 12, 2022 11:45 PM

May 02, 2022

Glyph Lefkowitz

Inbox Zero, Cost: Zero

One consistent bit of feedback that I’ve received on my earlier writing about email workflow is that I didn’t include a concrete enough set of instructions for getting started with task-management workflow, particularly with low-friction options that are available for people who don’t necessarily have $100 per year to drop on the cadillac of task-management applications.

Given that the piece seems to be enjoying a small resurgence of attention, I’ve significantly expanded the “Make A Place For Tasks” section of that article, with:

  • more no-cost, low-friction options for getting started (if you’re stuck on this step “if you use Gmail, just start using Google Tasks” is the main takeaway)
  • a guide for how to evaluate a task-management application for yourself, if you are trying to pick something that fits your work style better
  • several links to the specific “create a task from an email” tools and workflows for each app

It was nice to be doing this update now, because in the years since that piece was published, almost every major email application has added task-management features, or upgraded them into practical usability; gone are the times when properly filing your emails into clearly-described tasks was an esoteric feature that you needed expensive custom software for.

by Glyph at May 02, 2022 11:27 PM

April 28, 2022

Glyph Lefkowitz

You Should Compile Your Python And Here’s Why

In this post I’d like to convince you that you should be running Mypyc over your code1 — especially if your code is a library you upload to PyPI — for both your own benefit and that of the Python ecosystem at large.

But first, let me give you some background.

Python is Slow, And That’s Fine, Because It’s Fast Enough

A common narrative about Python’s value proposition, from the very earliest days of the language2, often recited in response to a teammate saying “shouldn’t we just write this in $HIGHER_PERFORMANCE_LANGUAGE instead?” goes something like this:

Sure, Python is slow.

But that’s okay, because it saves you so much time over implementing your code in $HIGHER_PERFORMANCE_LANGUAGE that you’ll have so much more time for optimizing those critical hot-spots where performance is really critical.

And if the language’s primitives are too slow to micro-optimize those hot-spots enough, that’s okay too, because you can always re-write just those small portions of the program as a C extension module.

Python’s got you covered!

There is some truth to this narrative, and I’ve quoted from it myself on many occasions. When I did so, I was not quoting it as some facile, abstract hypothetical, either. I had a few projects, particularly very early in my Python career, where I replaced performance-critical C++ code with a one tenth the number of lines of Python, and improved performance by orders of magnitude in the process3.

When you have algorithmically interesting, performance-sensitive code that can benefit from a high-level expressive language, and the resources to invest in making it fast, this process can be counterintuitively more efficient than other, “faster” tools. If you’re working on massively multiplayer online games4 or something equally technically challenging, Python can be a surprisingly good idea.

But… Is It Fine, Though?

This little nugget of folk wisdom does sound a bit defensive, doesn’t it? If Python were just fast, you could just use it, you wouldn’t need this litany of rationalizations. Surely if we believed that performance is important in our own Python code, we wouldn’t try to wave away the performance of Python itself.

Most projects are not massively multiplayer online games. On many straightforward business automation projects, this sort of staged approach to performance is impractical.

Not all performance problems are hot spots. Some programs have to be fast all the way through. This is true of some complex problems, like compilers and type checkers, but is also often the case in many kinds of batch processing; there are just a lot of numbers, and you have to add them all up.

More saliently for the vast majority of average software projects, optimization just isn’t in the budget. You do your best on your first try and hope that none of those hot spots get too hot, because as long as the system works within a painfully generous time budget, the business doesn’t care if it’s slow.

The progression from “idiomatic Python” to “optimized Python” to “C” is a one-way process that gradually loses the advantages that brought us to Python in the first place.

The difficult-to-reverse nature of each step means that once you have prototyped out a reasonably optimized data structure or algorithm, you need to quasi-permanently commit to it in order to squeeze out more straight-line performance of the implementation.

Plus, the process of optimizing Python often destroys its readability, for a few reasons:

  1. Optimized Python relies on knowledge of unusual tricks. Things like “use the array module instead of lists”, and “use % instead of .format”.
  2. Optimized Python requires you to avoid the things that make Python code nicely organized:
    1. method lookups are slow so you should use functions.
    2. object attribute accesses are slow so you should use tuples with hard-coded numeric offsets.
    3. function calls are slow so you should copy/paste and inline your logic
  3. Optimized Python requires very specific knowledge of where it’s going to be running, so you lose the flexibility of how to run it: making your code fast on CPython might make it much slower on PyPy, for example. Native extension modules can make your code faster, but might also make it fail to run inside a browser, or add a ton of work to get it set up on a new operating system.

Maintaining good performance is part of your software’s development lifecycle, not just a thing you do once and stop. So by moving into this increasingly arcane dialect of “fast” python, and then into another programming language entirely with a C rewrite, you end up having to maintain C code anyway. Not to mention the fact that rewriting large amounts of code in C is both ludicrously difficult (particularly if your team primarily knows Python) and also catastrophically dangerous. In recent years, safer tools such as PyO3 have become available, but they still involve switching programming languages and rewriting all your code as soon as you care about speed5.

So, for Python to be a truly general-purpose language, we need some way to just write Python, and have it be fast.

It would benefit every user of Python for there to be an easy, widely-used way to make idiomatic, simple Python that just does stuff like adding numbers, calling methods, and formatting strings in a straight line go really fast — exactly the sorts of things that are the slowest in Python, but are also the most common, particularly before you’ve had an opportunity to cleverly optimize.

We’ve Been Able To At Least Make Do

There are also a number of tools that have long been in use for addressing this problem: PyPy, Pyrex, Cython, Numba, and Numpy to name a few. Their maintainers all deserve tremendous amounts of credit, and I want to be very clear that this post is not intended to be critical of anyone’s work here. These tools have drawbacks, but many of those drawbacks make them much better suited to specialized uses beyond the more general 80% case I’m talking about in this post, for which Mypyc would not be suitable.

Each one of these tools impose limitations on either the way that you write code or where you can deploy it.

Cython and Numba aren’t really “Python” any more, because they require special-purpose performance-oriented annotations. Cython has long supported pure-Python type annotations, but you won’t get any benefit from telling it that your variable is an int, only a cython.int. It can’t optimize a @dataclass, only a @cython.cclass. And so on.

PyPy gets the closest — it’s definitely regular Python — but its strategy has important limitations. Primarily, despite the phenomenal and heroic effort that went into cpyext, it seems like there’s always just one PyPy-incompatible library in every large, existing project’s dependency list which makes it impossible to just drop in PyPy without doing a bunch of arcane debugging first.

PyPy might make your program magically much faster, but if it doesn’t work, you have to read the tea leaves on the JIT’s behavior in a profiler which practically requires an online component that doesn’t even work any more. So mostly you just simplify your code to use more straightforward data structures and remove CPython-specific tricks that might trip up the JIT, and hope for the best.

PyPy also introduces platform limitations. It’s always — understandably, since they have to catch up after the fact — lagging a bit behind the most recently released version of CPython, so there’s always some nifty language feature that you have to refrain from using for at least one more release cycle.

It also has architectural limitations. For example, it performs quite poorly on an M1 Mac since it still runs under x86_64 emulation on that platform. And due to iOS forbidding 3rd-party JITs, it won’t ever be able to provide better performance in one of the more constrained environments that needs it more that other places. So you might need to rely on CPython on those platforms anyway… and you just removed all your CPython-specific hacks to try to please the JIT on the other platforms you support.

So while I would encourage everyone to at least try their code on PyPy — if you’re running a web-based backend, it might save you half your hardware budget6 — it’s not going to solve “python is slow” in the general case.

It’ll Eventually Be All Right

This all sounds pretty negative, so I would be remiss if I did not also point out that the core team is well aware that Python’s default performance needs to be better, and Guido van Rossum literally came out of retirement for one last job to fix it, and we’ve already seen a bunch of benefits from that effort.

But there are some fundamental limitations on the long-term strategy for these optimizations; one of the big upcoming improvements is a JIT, which suffers from some (but not all) of the same limitations as PyPy, and the late-bound, freewheeling nature of Python inherently comes with some performance tradeoffs.

So it would still behoove us to have a strategy for production-ized code that gives good, portable, ahead-of-time performance.

But What About Right Now?

Mypyc takes the annotations meant for Mypy and generates C with them, potentially turning your code into a much more efficient extension module. As part of Mypy itself, it does this with your existing Python type-hints, the kind you’d already use Mypy with to check for correctness, so it doesn’t entail much in the way of additional work.

I’d been curious about this since it was initially released, but I still haven’t had a hard real-world performance problem to really put it through its paces.

So when I learned about the High Throughput Fizzbuzz Challenge via its impressive assembler implementation that achieves 56GiB/s, and I saw even heavily-optimized Python implementations sitting well below the performance of a totally naïve C reference implementation, I thought this would be an interesting miniature experiment to use to at least approximate practical usage.

In Which I Design A Completely Unfair Fight Which I Will Then Handily Win

The dizzying heights of cycle-counting hand-tuned assembler implementations of this benchmark are squarely out of our reach, but I wanted to see if I could beat the performance of this very naïve C implementation with Python that was optimized, but at least, somewhat idiomatic and readable.

I am about to compare a totally naïve C implementation with a fairly optimized hand-tuned Python one, which might seem like an unfair fight. But what I’m trying to approximate here is a micro-instance of the real-world development-team choice that looks like this:

Since Python is more productive, but slower, the effort to deliver each of the following is similar:

  1. a basic, straightforward implementation of our solution in C
  2. a moderately optimized Python implementation of our solution

and we need to choose between them.

This is why I’ll just be showing naïve C and not unrolling any loops; I’ll use -O3 because any team moderately concerned with performance would at least turn on the most basic options, but nothing further.

Furthermore, our hypothetical team also has this constraint, which really every reasonable team should:

We can trade off some readability for efficiency, but it’s important that our team be able to maintain this code going forward.

This is why I’m doing a bit of optimizing in Python but not going all out by calling mmap or pulling in numpy or attempting to use something super esoteric like a SIMD library to emulate what the assembler implementations do. The goal is that this is normal Python code with a reasonable level of systems-level understanding (i.e. accounting for the fact that pipes have buffers in the kernel and approximately matching their size maximizes throughput).

If you want to see FizzBuzz pushed to its limit, you can go check out the challenge itself. Although I think I do coincidentally beat the performance of the Python versions they currently have on there, that’s not what I’m setting out to do.

So with that elaborate framing of this slightly odd experiment out of the way, here’s our naïve C version:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <stdio.h>

int main() {
    for (int i = 1; i < 1000000000; i++) {
        if ((i % 3 == 0) && (i % 5 == 0)) {
            printf("FizzBuzz\n");
        } else if (i % 3 == 0) {
            printf("Fizz\n");
        } else if (i % 5 == 0) {
            printf("Buzz\n");
        } else {
            printf("%d\n", i);
        }
    }
}

First, let’s do a quick head-to-head comparison with a naïve Python implementation of the algorithm:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def fizzbuzz() -> None:
    for counter in range(1, 1000000000):
        fizz = counter % 3 == 0
        buzz = counter % 5 == 0
        if fizz:
            print("Fizz", end="")
        if buzz:
            print("Buzz", end="")
        if not (fizz or buzz):
            print(counter, end="")
        print()

if __name__ == "__main__":
    fizzbuzz()

Running both of these on my M1 Max MacBook, the naïve C implementation yields 127 MiB/s of Fizzbuzz output. But, as I said, although we’re not going to have time for testing a more complex optimized C version, we would want to at least build it with the performance benefits we get for free with the -O3 compiler option. It turns out that yields us a 27 MiB/s speedup. So 154 MiB/s is the number we have to beat.7

The naïve Python version achieves a dismal 24.3 MiB/s, due to a few issues. First of all, although it’s idiomatic, print() is doing a lot of unnecessary work here. Among other things, we are encoding Unicode, which the C version isn’t. Still, our equivalent of adding the -O3 option for C is running mypyc without changing anything, and that yields us a 6.8MiB/s speedup immediately. We still aren’t achieving comparable performance, but a roughly 25% performance improvement for no work at all is a promising start!8

In keeping with the “some optimizations, but not so much that it’s illegible” constraint described above, the specific optimizations I’ve chosen to pursue here are:

  1. switch to using bytes objects and sys.stdout.buffer to avoid encoding overhead
  2. take advantage of the repeating nature of the pattern in FizzBuzz output and pre-generate a template rather than computing each line independently
  3. fill out the buffer with the relevant integers from a sequence as we go
  4. tune the repetition of that template to a size that roughly fills a pipe buffer on my platform of choice

Hopefully, with that explanation, this isn’t too bad:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from sys import stdout
from typing import Tuple, Iterable


def precompute_template() -> Iterable[bytes]:
    for counter in range(1, 16):
        fizz = counter % 3 == 0
        buzz = counter % 5 == 0
        if fizz:
            yield b"Fizz"
        if buzz:
            yield b"Buzz"
        if not (fizz or buzz):
            yield b"%d"
        yield b"\n"


chunk_copies = 4
precomputed_template_chunks = list(precompute_template())
format_string = b"".join(precomputed_template_chunks)
number_indexes = [
    number_index
    for number_index, line_content in enumerate(format_string.split(b"\n"))
    if line_content == b"%d"
]
format_string *= chunk_copies


def fizzbuzz() -> None:
    num: int = 1
    output = stdout.buffer.write
    for num in range(1, 1000000001, 15 * chunk_copies):
        t: Tuple[int, ...] = tuple(
            (
                x + number_index
                for x in range(num, num + (15 * chunk_copies), 15)
                for number_index in number_indexes
            )
        )
        output(format_string % t)


if __name__ == "__main__":
    fizzbuzz()

Running this optimized version actually gets us within the ballpark of the naïve C version, even beating it by a hair; my measurement was 159 MiB/s, a small improvement even over -O3. So, per the “litany against C” from the beginning of this post, algorithmic optimization of Python really does help a lot; it’s not just a rationalization. This is a much bigger boost than our original no-effort Mypyc run, giving us more like an 85% speedup; definitely bigger than 25%.

But clearly we’re still being slowed down by Python’s function call overhead, object allocations for small integers, and so on, so Mypyc should help us out here: and indeed it does. On my machine, it nets a whopping 233 MiB/s. Now that we are accounting for performance and optimizing a bit, Mypyc’s relative advantage has doubled to a 50% improvement in performance on both the optimized-but-interpreted Python and naïve C versions.

It’s worth noting that the technique I used to produce the extension modules to test was literally pip install mypy; mypyc .../module.py, then python -c “import module”. I did already have a C compiler installed, but other than that, there was no setup.

I just wrote Python, and it just worked.

The Call To Adventure

Here’s what I want you to take away from all this:

  1. Python can be fast.
  2. More importantly, your Python can be fast.
  3. For a fairly small investment of effort, your Python code can be made meaningfully faster.

Unfortunately, due to the limitations and caveats of existing powerful performance tools like Cython and PyPy, over the last few years in the Python community a passive consensus has emerged. For most projects, in most cases, it’s just not worth it to bother to focus on performance. Everyone just uses the standard interpreter, and only fixes the worst performance regressions.

We should, of course, be glad that the standard interpreter is reliably getting faster all the time now, but we shouldn’t be basing our individual libraries’ and applications’ performance strategies on that alone.

The projects that care the most about performance have made the effort to use some of these tools, and they have often invested huge amounts of effort to good effect, but often they care about performance too much. They make the problem look even harder for everyone else, by essentially stipulating that step 1 is to do something extreme like give up and use Fortran for all the interesting stuff.

My goal with this post is to challenge that status quo, spark interest in revisiting the package ecosystem’s baseline performance expectations, and to get more projects — particularly libraries on PyPI — to pick up Mypyc and start giving Python a deserved reputation for being surprisingly fast.

The Last Piece of the Puzzle

One immediate objection you might be thinking of is the fact that, under the hood, Mypyc is emitting some C code and building it, and so this might create a problem for deployment: if you’ve got a Linux machine but 30% of your users are on Windows, moving from pure-Python to this hybrid workflow might create installation difficulties for them, or at least they won’t see the benefits.

Luckily a separate tool should make that a non-issue: cibuildwheel. “CI Build Wheel”, as its name suggests, lets you build your wheels in your continuous integration system, and upload those builds automatically upon tagging a release.

Often, the bulk of the work in using it is dealing with the additional complexities involved in setting up your build environment in CI to make sure you’re appropriately bundling in any native libraries you depend upon, and linking to them in the correct way. Mypyc’s limitation relative to Cython is a huge advantage here: it doesn’t let you link to other native libraries, so you can always skip the worst step here.

So, for maintainers, you don’t need to maintain a pile of janky VMs on your personal development machine in order to serve your users. For users, nobody needs to deal with the nightmare of setting up the right C compiler on their windows machine, because the wheels are prebuilt. Even users without a compiler who want to contribute new code or debug it can run it with the interpreter locally, and let the cloud handle the complicated compilation steps later. Once again, the fact that you can’t require additional, external C libraries here is a big advantage; it prevents you from making the user’s experience inadvertently worse.

cibuildwheel supports all major operating systems and architectures, and supported versions of Python, and even lets you build wheels for PyPy while you’re at it.9

Putting It All Together

Using Mypyc and cibuildwheel, we, as PyPI package maintainers, can potentially produce an ecosystem of much faster out-of-the-box experiences via prebuilt extension modules, written entirely in Python, which would make the average big Python application with plenty of dependencies feel snappier than expected. This doesn’t have to come with the pain that we have unfortunately come to expect from C extensions, either as maintainers or users.

Another nice thing is that this is not an all-or-nothing proposition. If you try PyPy and it blows up in some obscure way on your code, you have to give up on it unless you want to fully investigate what’s happening. But if you trip over a bug in Mypyc, you can report the bug, drop the module where you’re having the problem from the list of things you’re trying to compile, and move on. You don’t even have to start out by trying to jam your whole project through it; just pick a few key modules to get started, and gradually expand that list over time, as it makes sense for your project.

In a future post, I’ll try to put all of this together myself, and hopefully it’s not going to be embarrassingly difficult and make me eat my words.

Despite not having done that yet, I wanted to put this suggestion out now, to get other folks thinking about getting started with it. For older projects10, retrofitting all the existing infrastructure to put Mypyc in place might be a bit of a challenge. But for new projects starting today, putting this in place when there’s very little code might be as simple as adding a couple of lines to pyproject.toml and copy-pasting some YAML into a Github workflow.

If you’re thinking about making some new open source Python, give Mypyc a try, and see if you can delight some users with lightning speed right out of the box. If you do, let me know how it turns out.

Acknowledgments

Thanks to Donald Stufft, Moshe Zadka, Nelson Elhage, Itamar Turner-Trauring, and David Reid for extensive feedback on this post. As always, any errors or inaccuracies remain my own.


  1. Despite the fact that it is self-described “alpha” software; it’s clearly production-quality enough for Mypy itself to rely upon it, and to have thorough documentation, so if it has bugs that need fixing then it would be good to start discovering them. However, this whole post assumes that you do have good test coverage and you’ll be able to run it over your Mypyc-built artifacts; if you don’t, then this might be too risky. 

  2. I’d love to offer an attribution here, but I have no idea where it came from originally. It’s nearly impossible to search the web these days for things that people were saying in 2005... but as I recall, it grew up as a sort of oral tradition of call-and-response about performance complaints on forums and Usenet. 

  3. At least in the most favorable cases, of course. You can’t do this for everything, but in any sufficiently large C++ system you can always find some fun oversights

  4. as I was, at the time. 

  5. This is not to write off PyO3, which is an excellent tool. It has many uses beyond speed. Beyond the obvious one of “access to libraries in the excellent Rust ecosystem”, one of its important applications is in creating a safer lingua franca among high-level programming languages. If you have a large, complex, polyglot environment with some Ruby, some Java, some Python and some TypeScript, all of which need to share data structures, Rust is a much better option than C for the common libraries that you might need to bind in all of those languages. 

  6. It did for Twisted! We ran our website on absolutely ancient hardware for the better part of a decade and PyPy made it fast enough that nobody really noticed. When we were still using CPython, the site had become barely usable. 

  7. You can get this implementation and a table of the resgults here, on github

  8. I’m not sure that this is a meaningful comparison, but C’s no-cost optimization option of -O3 is a 20% improvement, so we’re in the same ballpark, which is interesting. 

  9. Interestingly, on PyPy, it might actually be faster to upload a pure-Python wheel anyway, since the higher cost of calling into a C module on that platform might negate any benefits of compiling it. But you’ll have to measure it and see. 

  10. not to put too fine a point on it, “like the ones that I maintain” 

by Glyph at April 28, 2022 10:11 PM

April 13, 2022

Thomas Vander Stichele

Running Anthos inside Google

"With everyone and their dog shifting to containers, and away from virtual machines (VMs), we realized that running vendor-provided software on VMs at Google was slowing us down. So we moved."

Bikram co-authored this blog post last year about DASInfra's experience moving workloads from Corp to Anthos. The group I run at work is going down a similar path by migrating VMs to Anthos on bare metal for on-prem.

Taken from The Playlist - a curated perspective on the intersection of form and content (subscribe, discuss)

flattr this!

by Thomas at April 13, 2022 11:41 PM

What’s the next action?

"Without a next action, there remains a potentially infinite gap between current reality and what you need to do."

David Allen's Getting Things Done is the non-fiction book I've reread the most in my life. I reread it every couple of years and still pick up on new ideas that I missed before, or parts that resonate better now and I'm excited to implement. Before Google, I used to give this book to new employees as a welcome gift.

The book got an update in 2015, and I haven't read the new version yet, so I'm planning an extended GTD book club at work in Q2, spreading the book out over multiple sessions. (In fact, I did just that for the young adult version of the book with my 16 year old godson back home in Belgium) If you've run a GTD book club, drop me a line!

Find out more at Getting Things Done® - David Allen's GTD® Methodology

"Too many meetings end with a vague feeling among the players that something ought to happen, and the hope that it’s not their personal job to make it so. [...] ask “So what’s the next action on this?” at the end of each discussion point in your next staff meeting"

Taken from The Playlist - a curated perspective on the intersection of form and content (subscribe, discuss)

flattr this!

by Thomas at April 13, 2022 11:40 PM

Rebecca Solnit – Men Explain Things to Me

"Most women fight wars on two fronts, one for whatever the putative topic is and one simply for the right to speak, to have ideas, to be acknowledged to be in possession of facts and truths, to have value, to be a human being."

In honor of International Women's Day 2022 (this past March 8th), some quotes from the 2008 article that inspired the term "mansplaining": to comment on or explain something to a woman in a condescending, overconfident, and often inaccurate or oversimplified manner.

I've certainly been (and probably still am) guilty of this behavior, and this is a standing invitation to let me know when I'm doing it to you.

Read the original article with a new introduction at Men Explain Things to Me – Guernica

"None was more astonishing than the one from the Indianapolis man who wrote in to tell me that he had “never personally or professionally shortchanged a woman” and went on to berate me for not hanging out with “more regular guys or at least do a little homework first,” gave me some advice about how to run my life, and then commented on my “feelings of inferiority.” He thought that being patronized was an experience a woman chooses to, or could choose not to have–and so the fault was all mine. Life is short; I didn’t write back."

Taken from The Playlist - a curated perspective on the intersection of form and content (subscribe, discuss)

flattr this!

by Thomas at April 13, 2022 11:39 PM

Draft emails from Google Docs

In the ever more vertical company that Google is becoming, it is even more important to collaborate on some of your communication - more people want to contribute to the message and get it right, and more thought needs to be given to the ever wider audience you're sending mails to.

A while back I copied over AppScript code from an internal Google project to send meeting notes to make a different tool which makes it easy to go from Google Docs draft to a mail in GMail and avoid embarrassing copy/paste errors. I'm happy to be able to retire that little side project in favor of a recently released built-in feature of Google Docs: Draft emails from Google Docs - Docs Editors Help

Taken from The Playlist - a curated perspective on the intersection of form and content (subscribe, discuss)

flattr this!

by Thomas at April 13, 2022 11:37 PM

March 31, 2022

Thomas Vander Stichele

The COVID Cocoon

"The global COVID-19 pandemic has had countless impacts on society. One interesting effect is that it has created an environment in which many people have been able to explore their gender identity and, in many cases, undergo a gender transition. As organizations return to in-person work, be it full-time or hybrid, there is a greater chance of “out” transgender, non-binary, or gender non-conforming employees in the workforce." (From the "5 Ally Actions Newsletter - Mar 25, 2022")

March 31 is the Transgender Day of Visibility. The COVID Cocoon is a nickname given for the phenomenon of people discovering their gender diversity during the pandemic environment.

The full report is an interesting read; one recommendation that we can all contribute to is on Culture and Communication: Proactively communicating that gender diversity is accepted, asking staff for their input, and being open and ready to listen helps create a culture where employees can feel safe, welcome, and valued.

Taken from The Playlist - a curated perspective on the intersection of form and content (subscribe, discuss)

flattr this!

by Thomas at March 31, 2022 11:47 PM

March 29, 2022

Thomas Vander Stichele

Building a Second Brain

"Your Second Brain is for preserving raw information over time until it's ready to be used, because information is perishable. Your Second Brain is the brain that doesn't forget." - Tiago Forte

Personal Knowledge Management is going through a wave of innovation with new tools like Roam, Logseq, Obsidian, Notion, RemNote, and others gaining traction over Evernote, OneNote and the like. It's a great time to get curious or reacquaint yourself with the tools and processes that strengthen learning, processing, and expressing your knowledge work.

The expression "Second Brain" has been popularized by Tiago Forte, who's been running an online cohort-based class called Building a Second Brain I took the class last year and found it a powerful distillation of an approach to PKM and note-taking. If you want to learn more, they just wrapped up the Second Brain Summit and posted all videos online: Second Brain Summit 2022 - Full Session Recordings - YouTube

The next class cohort is open for enrollment until March 30th midnight ET, at Building a Second Brain: Live 5-Week Online Course, and runs from April 12th to May 10th, 2022.

"Taking notes is the closest thing we have to time travel." - Kendrick Lamar

Taken from The Playlist - a curated perspective on the intersection of form and content (subscribe, discuss)

flattr this!

by Thomas at March 29, 2022 12:45 AM

March 05, 2022

Twisted Matrix Laboratories

Twisted 22.2.0 Release

On behalf of the Twisted contributors I announce the final release of
Twisted 22.2.0

This is a bugfix release.

The main bug is:

- CVE-2022-21716 twisted.conch.ssh.transport.SSHTransportBase now
disconnects the remote peer if the
SSH version string is not sent in the first 4096 bytes.

No new features were introduced in this release.

Release documentation is available at

  https://docs.twistedmatrix.com/en/twisted-22.2.0/

Wheels for the release candidate are available on PyPI

   https://pypi.org/project/Twisted/22.2.0/

   python -m pip install Twisted==22.2.0

Please use it and report any issues.

Many thanks to everyone who had a part in Twisted development,
the supporters of the Twisted Software Foundation,
the developers, and all the people testing and building great things
with Twisted!

Slava Ukraini!

by Adi Roiban (noreply@blogger.com) at March 05, 2022 10:18 AM

February 25, 2022

Thomas Vander Stichele

Time in Meetings

article #productivity

Meetings are both necessary and useful, but they fragment your week, your opportunity for flow, and you need non-meeting time for your output as a knowledge worker.

"Those of us on the maker's schedule are willing to compromise. We know we have to have some number of meetings. All we ask from those on the manager's schedule is that they understand the cost." - Paul Graham, Maker's Schedule, Manager's Schedule

flattr this!

by Thomas at February 25, 2022 06:26 PM

apenwarr@ – The Gift of It’s Your Problem Now

#article #log4j

"Sometimes the gift interprets JNDI strings in my log messages and executes random code from my LDAP server. This is the nature of gifts."

An interesting musing on the nature of gifts, big companies and startups, and free software, from apenwarr@

Taken from The Playlist - a curated perspective on the intersection of form and content (subscribe, discuss)

flattr this!

by Thomas at February 25, 2022 06:25 PM

Isabel Wilkerson – Caste: The Origins of Our Discontents

Isabel Wilkerson - Caste: The Origins of Our Discontents

#book #dei

"Like other old houses, [...] has an unseen skeleton, a caste system that is as central to its operation as are the studs and joists that we cannot see in the physical buildings we call home. Caste is the infrastructure of our divisions. It is the architecture of human hierarchy, the subconscious code of instructions for maintaining [...] a [...] social order."

Caste has taken the lead in my library as the most highlighted book, and is a deep exploration of Caste as the lens through which to see discrimination, drawing parallels between Europe, the United States, and India, providing a universal framing.

“Young people,” he said, “I would like to present to you a fellow untouchable from the United States of America.” King was floored. He had not expected that term to be applied to him. He was, in fact, put off by it at first.

Taken from The Playlist - a curated perspective on the intersection of form and content (subscribe, discuss)

flattr this!

by Thomas at February 25, 2022 06:24 PM

9 Ways We Self Sabotage

9 Ways We Self Sabotage

#survey #productivity

"Saboteurs are the voices in your head that generate negative emotions in the way you handle life’s everyday challenges. They represent automated patterns in your mind for how to think, feel, and respond. They cause all of your stress, anxiety, self-doubt, frustration, restlessness, and unhappiness. They sabotage your performance, wellbeing, and relationships."

Positive Intelligence is a mental fitness framework and, among other concepts, taught me helpful practical ways to deal with stress, both professionally and personally.

Take the test or read more on How we self-sabotage

Taken from The Playlist - a curated perspective on the intersection of form and content (subscribe, discuss)

flattr this!

by Thomas at February 25, 2022 06:24 PM

Glyph Lefkowitz

Legitimizing Blockchain

Yesterday, 1Password made the following announcement:

I am very unhappy about this.

As of this writing, the replies to this announcement are, by my count, roughly 95% paying customers who are furious with them for doing this, 3% scammers who are jubilant that this is popularizing their scamming tool of choice, and about 2% blockchain-enthusiasts expressing confusion as to why everyone is so mad.

Scanning through that 2%’s twitter bios and timelines, I could see content other than memes and shilling, so it seemed at least plausible to me that these people are scam victims who haven’t gotten to the blow-off yet, and their confusion is genuine. Given that “why is everyone so mad” is a much less intense reaction than fury or jubilation, I assume that many others read through some of the vitriol and had this reaction, but then didn’t post anything themselves.

This post is for two audiences: that 2%, genuinely wondering what the big deal is, and also those who have a vague feeling that cryptocurrency is bad, but don’t see the point of making much of a fuss about it.

This is why we should make a fuss about it.


The objection most often raised in the comments went something like this:

This is just a feature that you don’t like; if it’s not for you, just don’t use it. Why yell at 1Password just for making a feature that makes someone else happy?

To begin with, the actual technical feature appears to be something related to auto-filling in browser-extension UI, which is fine. I don’t object to the feature. I don’t even object to features which explicitly help people store cryptocurrency more securely, as a harm reduction measure.

Also, to get this out of the way now: cryptocurrency is a scam. I’m not going to argue the case for that here. Others have made the argument far more exhaustively, and you can read literally hundreds of pages and watch hours of video explaining why by clicking here.

The issue is with the co-marketing effort: the fact that 1Password is using their well-respected brand to help advertise and legitimize scam-facilitation technology like Solana and Phantom.

Even if we were to accept all this, it’s a scam, 1Password is marketing it, etc, my hypothetical blockchain-curious interlocutor here might further object:

What’s the big deal about legitimizing these things, even if they are fraud? Surely you can just not get defrauded, and ignore the marketing?

That’s true, but it also misses the point: legitimizing and promoting these things does various kinds of harm.

More broadly, although I’m writing about 1Password’s specific announcement here, and a small amount of the reasoning will be specific to password management tools, most of the concerns I’ll describe are fairly general to any company promoting or co-marketing with cryptocurrency, and thus hopefully this post will serve for future instances where we should tell some other company to stop supporting blockchains as well.

So with all that out of the way, here are some of the harms that one might be concerned about, from the least selfish concern to the most.


Concern #1: the well-being of others

I don’t know how to explain to you that you should care about other people, but if you do care about other people, this could hurt them.

First and foremost, the entire scam of cryptocurrency rests upon making people believe that the assets are worth something. Most people are not steeped in the technical minutiae of blockchains, and tend to trust things based on institutional reputation. 1Password has a strong brand, related to information security, and they’re saying that cryptocurrencies are good, so it’s likely to convince a nonzero number of people to put their money into this technology that has enormous non-obvious risks. They could easily lose everything.

Advertising 1Password in this way additionally encourages users to maintain custody of their own blockchain assets on their own devices. Doing so with 1Password is considerably less risky than it might be otherwise, so if this were to only reach people who were already planning to store their wallets on their own computers, then great.

However, this might encourage users who had not previously thought to look at cryptocurrency at all to do so, and if they found it via 1Password they might start using 1Password to store their first few secrets. Storing them in this way, although less risky, is still unreasonably risky, given the lack of any kind of safety mechanisms on blockchain-backed transactions. Even if they’re savvy enough not to get scammed, nobody is savvy enough not to get hacked, particularly by sophisticated technical attacks which are worth leveraging against high-value targets like people with expensive crypto wallets on their computers.

To be clear, crypto exchanges are, on average, extremely bad at the job of not getting their users money stolen, but individual users are likely to be even worse at that job.

Concern #2: economic damage

If you don’t care about other people much, but you still care about living in a functioning society, then the promotion of blockchain based financial instruments is a huge destabilization risk. As Dan Olson explains in the devastating video essay / documentary Line Goes Up, blockchain-based financial instruments share a lot of extremely concerning properties that made mortgage-backed securities and collateralized debt obligations so financially toxic in the 2008 crash. Large-scale adoption of these things could lead to a similar crisis, or even worse, a global deflationary spiral in the style of the one that caused the great depression, setting off the kind of economic damage that could result in mass famine and mass death.

Of course, any individual company or celebrity advertising crypto is not going to trigger an immediate economic collapse. Each of these is a snowflake in an avalanche. I have no illusions that convincing just 1Password to stop this is going to turn the tide of the entire blockchain catastrophe that is unfolding all around us, or indeed that my one little post here is going to make the decisive difference between, 1Password stopping vs. not.

But that’s exactly why I’m trying to persuade you, dear reader, that this is a big deal and we should all try to work together to stop it.

Concern #3: environmental damage

While this specific blockchain is “greener” than others, but given the huge proportion of cryptocurrency generally that is backed by electrical waste, and the cultural and technical incentives that make trading one blockchain asset for another more common than cashing out to dollars, it’s still a legitimate concern that promoting blockchain in general will promote environmental destruction indirectly.

Furthermore, the way that Solana is less energy-intensive than other blockchains is by using proof-of-stake, so there’s a sliding scale here between economic and environmental damage, given that proof-of-stake is designed to accelerate wealth accumulation among non-productive participants, and thereby encourages hoarding. So the reduction in environmental damage just makes the previous point even worse.

Concern #4: increased targeting risk

Even if you’re a full blown sociopath with no concern for others and an iron-clad confidence that you can navigate the collapse of the financial system without any harm to you personally, there is still a pretty big negative here: increased risk from threat actors. Even if you like and use blockchain, and want to use this feature, this risk still affects you.

If 1Password happened to have some features that blockchain nerds could use to store their secrets, then attackers might have some interest in breaking in to 1Password, and could possibly work on tools to do so. That’s the risk of existing on the Internet at all. But if 1Password loudly advertises, repeatedly, that they are will be integrating with a variety of cryptocurrency providers, then this will let attackers know that 1Password is the preferred cryptocurrency storage mechanism.

This further means that attackers will start trying to figure out ways to target 1Password users, on the assumption that we’re more likely to have crypto assets lying around on our filesystems; not only developing tools to break in to 1Password but developing tools to fingerprint users who have the extension installed, who have accounts on the service, whose emails show up on the forum, etc.

Now, of course, 1Password users keep plenty of high-value information inside 1Password already; that’s the whole point. But cryptocurrency is special because of the irreversible nature of transactions, and the immediacy of the benefit to cybercriminals specifically.

If you steal all of someone’s bank passwords, you could potentially get a bunch of their money, but it is expensive and risky for the criminals. The transactions can be traced directly to actual human account holders immediately; anti-money-laundering regulations mean that this can usually be accomplished even across international borders. Transfers can be reversed.

This discrepancy between real money and cryptocurrency is exactly why ransomware was created by cryptocurrency. It makes cryptocurrency attractive specifically to the kinds of people who have expertise and resources to mount wide-spectrum digital attacks against whole populations.

Of course, if they develop tools to fingerprint and hack 1Password users, but they don’t luck out and find easy-to-steal crypto on your computer, they might as well try to steal other things of value, like your identity, credit information, and so on. These are higher-risk, but now that they’ve built all that infrastructure and hacked all these machines, there’s a big sunk cost that makes it more worthwhile.

Please Stop

I really hope that 1Password abandons this destructive scheme. Even if they fully walk this back, I will still find it much harder to recommend their product in the future; there will need to be some active effort to repair trust with their user community. If I’ve convinced you of the problems here, please let them know as a reply to the tweet, the email linked from their blog post, their community forum, or the Reddit post of the announcement, so that they can get a clear signal that this is unacceptable.

by Glyph at February 25, 2022 06:20 AM

A Better Pygame Mainloop

This post recommends calling pygame.display.flip from a thread, which I tested extensively on mac, windows, and linux before posting, but after some feedback from readers, I realize that this strategy is not in fact cross-platform; specifically, the nvidia drivers on linux appear to either crash or display a black window if you try to do this. The SDL FAQ does say that you can’t call “video functions” from multiple threads, and flip does do that under the hood. I do plan to update this post again, either with a method to make it safe, or a method to use slightly more complex timing heuristics to accomplish the same thing. In the meanwhile, please be aware that this may cause portability problems for your code.

I’ve written about this before, but in that context I was writing mainly about frame-rate independence, and only gave a brief mention of vertical sync; the title also mentioned Twisted, and upon re-reading it I realized that many folks who might get a lot of use out of its technique would not have bothered to read it, just because I made it sound like an aside in the context of an animation technique in a game that already wanted to use Twisted for some reason, rather than a comprehensive best practice. Now that Pygame 2.0 is out, though, and the vsync=1 flag is more reliably available to everyone, I thought it would be worth revisiting.


Per the many tutorials out there, including the official one, most Pygame mainloops look like this:

1
2
3
4
5
6
7
8
pygame.display.set_mode((320, 240))

while 1:
    for event in pygame.event.get():
        handleEvent(event)
    for drawable in myDrawables:
        drawable.draw()
    pygame.display.flip()

Obviously that works okay, or folks wouldn’t do it, but it can give an impression of a certain lack of polish for most beginner Pygame games.

The thing that’s always bothered me personally about this idiom is: where does the networking go? After spending many years trying to popularize event loops in Python, I’m sad to see people implementing loops over and over again that have no way to get networking, or threads, or timers scheduled in a standard way so that libraries could be written without the application needing to manually call them every frame.

But, who cares how I feel about it? Lots of games don’t have networking1. There are more general problems with it. Specifically, it is likely to:

  1. waste power, and
  2. look bad.

Wasting Power

Why should anyone care about power when they’re making a video game? Aren’t games supposed to just gobble up CPUs and GPUs for breakfast, burning up as much power as they need for the most gamer experience possible?

Chances are, if you’re making a game that you expect anyone that you don’t personally know to play, they’re going to be playing it on a laptop2. Pygame might have a reputation for being “slow”, but for a simple 2D game with only a few sprites, Python can easily render several thousand frames per second. Even the fastest display in the world can only refresh at 360Hz3. That’s less than one thousand frames per second. The average laptop display is going to be more like 60Hz, or — if you’re lucky — maybe 120. By rendering thousands of frames that the user never even sees, you warm up their CPU uncomfortably4, and you waste 10x (or more) of their battery doing useless work.

At some point your game might have enough stuff going on that it will run the CPU at full tilt, and if it does, that’s probably fine; at least then you’ll be using up that heat and battery life in order to make their computer do something useful. But even if it is, it’s probably not doing that all of the time, and battery is definitely a use-over-time sort of problem.

Looking Bad

If you’re rendering directly to the screen without regard for vsync, your players are going to experience Screen Tearing, where the screen is in the middle of updating while you’re in the middle of drawing to it. This looks especially bad if your game is panning over a background, which is a very likely scenario for the usual genre of 2D Pygame game.

How to fix it?

Pygame lets you turn on VSync, and in Pygame 2, you can do this simply by passing the pygame.SCALED flag and the vsync=1 argument to set_mode().

Now your game will have silky smooth animations and scrolling5! Solved!

But... if the fix is so simple, why doesn’t everybody — including, notably, the official documentation — recommend doing this?

The solution creates another problem: pygame.display.flip may now block until the next display refresh, which may be many milliseconds.

Even worse: note the word “may”. Unfortunately, behavior of vsync is quite inconsistent between platforms and drivers, so for a properly cross-platform game it may be necessary to allow the user to select a frame rate and wait on an asyncio.sleep than running flip in a thread. Using the techniques from the answers to this stack overflow answer you can establish a reasonable heuristic for the refresh rate of the relevant display, but if adding those libraries and writing that code is too complex, “60” is probably a good enough value to start with, even if the user’s monitor can go a little faster. This might save a little power even in the case where you can rely on flip to tell you when the monitor is actually ready again; if your game can only reliably render 60FPS anyway because there’s too much Python game logic going on to consistently go faster, it’s better to achieve a consistent but lower framerate than to be faster but inconsistent.

The potential for blocking needs to be dealt with though, and it has several knock-on effects.

For one thing, it makes my “where do you put the networking” problem even worse: most networking frameworks expect to be able to send more than one packet every 16 milliseconds.

More pressingly for most Pygame users, however, it creates a minor performance headache. You now spend a bunch of time blocked in the now-blocking flip call, wasting precious milliseconds that you could be using to do stuff unrelated to drawing, like handling user input, updating animations, running AI, and so on.

The problem is that your Pygame mainloop has 3 jobs:

  1. drawing
  2. game logic (AI and so on)
  3. input handling

What you want to do to ensure the smoothest possible frame rate is to draw everything as fast as you possibly can at the beginning of the frame and then call flip immediately to be sure that the graphics have been delivered to the screen and they don’t have to wait until the next screen-refresh. However, this is at odds with the need to get as much done as possible before you call flip and possibly block for 1/60th of a second.

So either you put off calling flip, potentially risking a dropped frame if your AI is a little slow, or you call flip too eagerly and waste a bunch of time waiting around for the display to refresh. This is especially true of things like animations, which you can’t update before drawing, because you have to draw this frame before you worry about the next one, but waiting until after flip wastes valuable time; by the time you are starting your next frame draw, you possibly have other code which now needs to run, and you’re racing to get it done before that next flip call.

Now, if your Python game logic is actually saturating your CPU — which is not hard to do — you’ll drop frames no matter what. But there are a lot of marginal cases where you’ve mostly got enough CPU to do what you need to without dropping frames, and it can be a lot of overhead to constantly check the clock to see if you have enough frame budget left to do one more work item before the frame deadline - or, for that matter, to maintain a workable heuristic for exactly when that frame deadline will be.

The technique to avoid these problems is deceptively simple, and in fact it was covered with the deferToThread trick presented in my earlier post. But again, we’re not here to talk about Twisted. So let’s do this the no-additional-dependencies, stdlib-only way, with asyncio:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import asyncio
import time
from math import inf

from pygame.display import set_mode, flip
from pygame.constants import SCALED
from pygame.event import get

event_handler = ...
drawables = [...]

async def pygame_loop(framerate_limit=inf):
    loop = asyncio.get_event_loop()
    screen_surface = set_mode(size=(480, 255), flags=SCALED, vsync=1)
    next_frame_target = 0.0
    limit_frame_duration = (1.0 / framerate_limit)

    while True:

        if limit_frame_duration:
            # framerate limiter
            this_frame = time.time()
            delay = next_frame_target - this_frame
            if delay > 0:
                await asyncio.sleep(delay)
            next_frame_target = this_frame + limit_frame_duration

        for drawable in drawables:
            drawable.draw(screen_surface)
        events_to_handle = list(get())
        events_handled = loop.create_task(handle_events(events_to_handle))
        await loop.run_in_executor(None, flip)
        # don’t want to accidentally start drawing again until events are done
        await events_handled

async def handle_events(events_to_handle):
    # note that this must be an async def even if it doesn’t await
    for event in events_to_handle:
        event_handler.handle_event(event)

asyncio.run(pygame_loop(120))

Go Forth and Loop Better

At some point I will probably release my own wrapper library6 which does something similar to this, but I really wanted to present this as a technique rather than as some packaged-up code to use, since do-it-yourself mainloops, and keeping dependencies to a minimum, are such staples of Pygame community culture.

As you can see, this technique is only a few lines longer than the standard recipe for a Pygame main loop, but you now have access to a ton of additional functionality:

  • You can manage your framerate independence in both animations and game logic by just setting some timers and letting the frames update at the appropriate times; stop worrying about doing math on the clock by yourself!
  • Do you want to add networked multiplayer? No problem! Networking all happens inside the event loop, make whatever network requests you want, and never worry about blocking the game’s drawing on a network request!
  • Now your players’ laptops run cool while playing, and the graphics don’t have ugly tearing artifacts any more!

I really hope that this sees broader adoption so that the description “indie game made in Python” will no longer imply “runs hot and tears a lot when the screen is panning”. I’m also definitely curious to hear from readers, so please let me know if you end up using this technique to good effect!7


  1. And, honestly, a few fewer could stand to have it, given how much unnecessary always-online stuff there is in single-player experiences these days. But I digress. That’s why I’m in a footnote, this is a good place for digressing. 

  2. “Worldwide sales of laptops have eclipsed desktops for more than a decade. In 2019, desktop sales totaled 88.4 million units compared to 166 million laptops. That gap is expected to grow to 79 million versus 171 million by 2023.” 

  3. At least, Nvidia says that “the world’s fastest esports displays” are both 360Hz and also support G-Sync, and who am I to disagree? 

  4. They’re playing on a laptop, remember? So they’re literally uncomfortable. 

  5. Assuming you’ve made everything frame-rate independent, as mentioned in the aforementioned post

  6. because of course I will 

  7. And also, like, if there are horrible bugs in this code, so I can update it. It is super brief and abstract to show how general it is, but that also means it’s not really possible to test it as-is; my full-working-code examples are much longer and it’s definitely possible something got lost in translation. 

by Glyph at February 25, 2022 05:38 AM

February 20, 2022

Moshe Zadka

February 18, 2022

Thomas Vander Stichele

You are not my papa

Last night I rode our bike home from Brooklyn, with my daughter crying loudly "You are not my papa!" most of the way.

We were a few minutes late picking her up from her class, and she was the last one there, crying in the arms of the teacher, and yelling something loudly, too loud to understand.
I picked her up, hugged her, asked what's wrong and tried to calm her down, but she wasn't having it. I put her in the back of our bike, strapping her in, checking with my son what she could be saying. We finally started making out that she was saying "not my papa".

I tried to convince her that I am, in fact, her papa, but she just kept repeating the same thing. We started our ride back home, and at the first red light I was acutely aware of her still yelling the same thing while standing still in traffic next to other bikes. What would I do if I was stuck in traffic next to a vehicle with a crying child yelling "You are not my papa?", I wondered. I started asking her questions like, "what hair color does your papa have?" to get her to stop and think, and I would respond, "that's interesting, just like me". I'd ask a few questions like that until the lights turned green.

I was hoping this would work for all the stops on our 25 minute ride home, and I was hoping we'd not run into any police cars along the way, just in case. Of course, two minutes later, I was parallel with a string of five police cars, all with their lights flashing. I kept repeating the questions at every stop, until she fell asleep as she usually does on the bike.

She slept all the way through dinner, and the next morning at breakfast I asked her, "who's your papa?" And she beamed at me and yelled, "you are my papa!"

My best guess at what happened is that at pickup she saw a string of papas pick up their kids, but didn't see me, and started saying "you are not my papa" at every other papa, until I was the last one to show up. I'll never show up last again.

flattr this!

by Thomas at February 18, 2022 01:30 AM

February 09, 2022

Twisted Matrix Laboratories

Twisted 22.1.0 Final Release Announcement

Hi

On behalf of the Twisted contributors I announce the final release of Twisted 22.1.0

This is mainly a bugfix release.

The main bug is:

  • CVE-2022-21712 / GHSA-92x2-jw7w-xvvx twisted.web.client.RedirectAgent and twisted.web.client.BrowserLikeRedirectAgent now properly remove sensitive headers when redirecting to a different origin. (#10294)

The only new feature is:

  • twisted.conch.ssh now supports SSH extension negotiation (RFC 8308). (#10266)

The release and NEWS file is available for review at


Release documentation is available at


Wheels for the release candidate are available on PyPI


  python -m pip install Twisted==22.1.0

Please use it and report any issues.

Many thanks to everyone who had a part in Twisted development, the supporters of the Twisted Software Foundation, the developers, and all the people testing and building great things with Twisted!

— Adi Roiban

by glyph (noreply@blogger.com) at February 09, 2022 07:51 PM

February 05, 2022

Hynek Schlawack

typing.Protocol Across Python Versions

How to seamlessly support typing.Protocol on Python versions older and newer than 3.8. At the same time.

by Hynek Schlawack (hs@ox.cx) at February 05, 2022 05:00 PM

January 22, 2022

Thomas Vander Stichele

Quick way to process an Inbox folder in Obsidian

Obsidian's Gems of the Year 2021 nomination has been a great source of cool ideas to add tweaks to my Obsidian setup.

In particular, Quick Capture (mac/iOS) and Inbox Processing was a great gem to uncover as I try and implement the weekly review stage of my Second Brain/PARA setup!

I noticed that the archive/move script was a little slow, taking several seconds to open up the dialog for selecting a folder, breaking my flow. I checked the code and noticed it built a set of folders recursively.

I simplified the code for my use case, removing the archive folder path, and using the file explorer's built in move dialog (which is much faster) and a callback to advance.

The resulting gist is Obsidian: Archive current file and then open next file in folder (Templater script) · GitHub

I'm sure it could be improved further if I understood the execution, variable scope, and callback model better, but this is good enough for me!

I get very little coding time these days, and I hate working in an environment I haven't had a chance to really master yet. It's all trial and error through editing a javascript file in a markdown editor with no syntax highlighting. But it's still a nice feeling when you can go in and out of a code base in a few hours and scratch the itch you had.

flattr this!

by Thomas at January 22, 2022 10:14 PM

December 28, 2021

Hynek Schlawack

import attrs

An attempt at catharsis. This is a deeply personal blog post about the most influential project I’ve ever created: attrs, the progenitor of modern Python class utilities. I’m retelling its history from my perspective, how it begot dataclasses, and how I’m leading it into the future.

by Hynek Schlawack (hs@ox.cx) at December 28, 2021 12:00 AM