Yank on that thread harder, developers and engineers are not safe just because they rely on different patterns.
But once you've toppled the house of knowledge work, you'll find you can rebuild it easily because the foundation still exists, and it's the thing missing in this analysis; judgement.
Optimal judgement is worth a premium... in some cases. In other cases, it's sufficient to merely keep trundling. Numerically more companies are boring than exciting.
MBAs are just components, and the cost of that component just went down.
I don't think it's common knowledge how much they dropped imports since the Iran / straight of hormuz crisis.
Back at the start of that conflict there were a lot of breathless takes about what catastrophe was in store in a few weeks time, and what materialised was bad but not in anything like the same league as what was predicted.
The thing that wasn't accounted for in the analysis at the time was just how rapidly and by how much China could suppress its appetite for oil imports.
They had basically been planning for this shortfall for ever as a likely outcome of sanctions/blockades during an invasion of Taiwan.
Curtailing imports so much is a smart soft power move in not only does it ingratiate countries that would suffer most from the price spike but it could soften the resolve of Taiwan in showing that one of the largest penalties that would result from the invasion isn't nearly as punishing which in turn makes Taiwan's porcupine strategy less viable.
Along with US own goals like the thoroughly bungled and unplanned invasion, tariffs, corruption (both business crimes and more moral crimes like pedophilia or disregard for following legal process) it's made most of the west rate China above USA in favourability [latest Pew research poll].
System 1: a system for recording an image of a location at a given place over time.
System 1 can refer to the dashcam or the mk1 eyeball + notepad in your example.
System 2: a system for tracking the presence of a person across both time and location
An example of system 2 would be the facial id system being trialled on the london underground currently.
These are a difference in kind not in degree. It doesn't matter how many system 1's you deploy, you cannot unlock the capability of querying where any given face was observed across time and location.
Where did you learn that? It doesn't reflect the structural volumes present. Central banks make interventions all the time in line with little stabilisation programs. Those are almost always deemed success.
Maybe you deduced it by yourself? If so, fx is weird despite traditionally being seen as the simplest area in finance. E.g. It's counter-intuitive but we tend to think trade make up most FX volume globally. It's in the area of less than 3%. The majority by far is speculative and hedging.
The other trap is fx volume, people assume the know what volume is but then they learn expressions of fx volume is almost always actually tick volume.
It works very well in rich countries, but it is expensive, especially if the volume of exchanged currency is low. This is one of the reason behind the Euro, countries realised it would be way less expensive to defend the currency if they banded together. This is also a big why ex french colonies keeps colonial money, because that currency is pegged to the Euro and that peg protect it (the only way to get out cheaply is to do a West African Union or something similar, which is why anti-colonialists are big on the subject. They are right).
There are so many footguns[1] in async python but it really has stolen the zeitgeist of modern python web. Synchronous django has a lot to commend it. If you deploy it reasonably well (workers, behind a caching reverse proxy etc) it's easy to operate in production even under duress.
It's not going to be the right stack for long lived websocket connections or whatever but for a CRUD-ish or enterprise app, often very productive.
[1] buffer bloat is too easy to sleep walk into
queue = asyncio.Queue() # oops
unbounded concurrency
await asyncio.gather(*(fetch(item) for item in items)) # look mum! no outbound sockets left or maybe even no file descriptors
accidental blocking
data = requests.get(url).json() # oops! we're blocking the event loop
and sure you can setup a separate task to measure the event loop latency and alert on that but this is the kinda thing you'll never find in a tutorial, you just need to experience this stuff and figure out a solution you like
I can go on and on about async python (cancellation bugs, leaking a task - that has a cataclysmic failure mode where if you forget to hold a reference to your asyncio.create_task() then it's all weak refs in that machinery so your task can get garbage collected before it ran or completed in production! super tricky forensics. Then there's all the obvious stuff like race conditions, new ways of creating deadlocks, blah blah i really can go on for days.
Everytime I have to deal with websockets in Django, I want to rip my eyeballs out.
Most of the time what I really want is just push support, and don't need bidirectional communication, i.e. SSE. But it's the same thing: both are a pain to work with in Django. I find django-channels so cumbersome.
So, for my latest projects, I've ended up offloading this push functionality to a service like Centrifugo. Django talks to Centrifugo, and clients connect to it as well. It makes Django still be django, without worrying about django-channels, ASGI and all of that complicated setup. I've been very, very, very happy with this decision, even if I do have some additional complexity to articulate the two. It is nowhere near as confusing and convoluted as django-channels and uvicorn/daphne, though.
As for async python in general, fully agree. Every couple of years I decide to give it a go with some new tool or framework, thinking surely now it'll finally just click and I'll get with the times. Every time I feel disappointed with footguns and absolutely terrible ergonomics. No, thanks.
Please go on. I also feel like asyncio is a big hack. Even just accidentally blocking the event loop is way too easy. And I couldn't believe it when I read that you need to store a reference to a task in a set to keep it from accidentally getting canceled. How did this become the main stack of AI backends? It's like node but slower AND much easier to mess up because of synchronous IO.
Well references in python are mutable by default, so you essentially combine an asynchronous model with mutable shared state. Combine that model with unknown caller exceptions in any subfunction and you're in for a world of hurt.
I'm not sure i see it as a hack, but i do feel unduly burdened as a developer by async in python. I feel like there's a lot that i have to think about and i'd appreciate more help from the language.
I don't want to be overly critical, there's languages that people complain about and then there are languages that no one uses... If i compare it to js/ts, some stuff is genuinely better in Python - e.g. if you missed an await. While both ecosystems have lint tools available for this, but the behaviour is just friendlier in python.
Structured concurrency is better in python, but even if TaskGroup is nicer to use than AbortController, it still has its own foibles which means i'll usually advocate for AnyIO.
But the js/ts ecosystem just generally benefits from being async from the get-go where python you're just a time.sleep() away from a bug that will slip through dev envs and ci pipelines undetected and only rear its head under load in prod.
If i have a tip to share its the debug flag for asyncio run:
asyncio.run(f(), debug=True) # find some more issues before prod
I agree that there's plenty of footguns with async python, but I think the hype is a bit overblown. A few sensible defaults in the standard library would go a long way, e.g. requiring a maxsize argument to Queue (or None if you really need it to be unbounded). The "accidental blocking" thing seems like less of design issue - it has much more to do with python's super long history as a sync-first language. Taking a step back, though, async is popular because it is genuinely extremely useful! If you need to do a bunch of work and that work is mostly IO-bound, then it makes sense to live in an async world.
I think people in the Python world just don't yet have a good mental model of async either. I recently have had conversations with a number of people who have switched to FastAPI because "it's faster" even when they're using it to serve ML models which are compute bound and there's nothing you can await on.
I think the mental model is the right thing to focus on. I'm not denying there are cases where async at the edge of an app can make sense but there's no free lunch and i that doesn't appear to be well understood.
Maybe i will write a blog post, if nothing else than to get my own thoughts in order.
async python is always slower than just naively using threads. I don't think there is any application in production anywhere that would not prove me right on this. the complete lack of any kind of scheduling is a disaster. unless you know exactly what I mean by a scheduler please don't reply to this.
I don’t think there’s anything Python specific about that criticism. Most languages with cooperative concurrency have simple or no schedulers and run coroutines pretty much directly (or unpredictably, if they’re scheduled into parallel executors). Even Java and Tokio’s scheduling, such as it is, isn’t that directly controllable or observable and doesn’t magically fix loop-blocking or cache efficiency or whatever.
I think you might just not like cooperative async IO multiplexing as a programming model. Which is fine, but has nothing to do with Python.
I don't like it, but I don't like it because it is impossible to make it performant.
What if I told you there was one of the greatest engineering achievements of mankind ready to make sure your threads run whenever they can, but also fairly and carefully balancing context switching cost and throughput, and instead you use python async and get 1/10th the performance, weird impossible to avoid pauses an order of magnitude longer than the threaded version's median response time, and just constantly have to fix bugs about callbacks firing on canceled blah blah blah.
Those are all fair points! They apply more or less equally to, say, nodejs or Perl's AnyEvent.
I'm totally onboard with "use the OS thread scheduler when you can"; it's a brilliant piece of technology. And if you are in a situation where
> pauses [are] an order of magnitude longer than the threaded version's median response time
...then fully cooperative, single-thread async programming isn't for you!
But async programming was made for the inverse use-case of that: backend functions which spend the vast majority of their time waiting for I/O--slow databases, HTTP requests, and whatnot--and which needed to serve very high concurrency demands on very cheap servers. If, say, your web route handler wants to probe or parallel-fetch from 100 unique-per-request HTTP endpoints, and you expect to handle 100 web requests concurrency, using threads is going to cost a lot of resources (the portion stack space that's allocated eagerly, start/stop time, and so on). For some folks, those resources might be readily available, in which case threads may still be the way to go! But for other folks (tiny servers, or more orders of magnitude than 100/100--use cases like proxies or CDNs) cooperative async might be a better way to go. Similarly, if your concurrent code is sharing a lot of data between routines but is primarily waiting on I/O, single-thread async concurrency might be a less race-condition-bug-prone model to use.
I'm generally with you that it's a tool that's prioritized above threads in a lot of cases when it shouldn't be. But it is still definitely valuable for a lot of common challenges.
You clearly know what you are talking about and I agree with what you are saying 100%, if you are spending almost all your time waiting around, you should address that. Generally the OS has a better solution there as well, in fact linux has several. Python also has several, and solutions like Stackless or Twisted are just a joy compared to the cancer of python async.
Even if you buy into callback-style async style programming like node or python async, Python async is not just bad because the general idea of it is bad, it is also bad-at-what-it-is-trying-to-be. It's a bad, hack, incompetent implementation of async programming, which in this style (cooperative callback style) is a bad architecture. It's unforgivably bad. Look in the stdlib and read the code for it, it's full of gotchas and confusion and patch after patch after patch putting in exceptions (like the "we want to do W but if this task is x then actually don't do this but if it's y do this other thing and if it's z then do a third thing kind of exception) and checks and double checks every 3rd line because the design itself is broken. Whole features (like un-cancel task) were put in because one influential person demanded it because the library they wrote can't work without it, even though it breaks a fundamental invariant that should never be violated.
Fundamentally, when you choose a tool or architecture, it should not be the thing you spend 50% of your time debugging. Every Python async project I've ever worked on required me to spend half my time tracing around in the guts of the async library itself to figure out what weird edge case we were running into that made something completely broken. This is like the hallmark of bad software.
I wish Stackless/Twisted had succeeded, I really do, but they unfortunately lost to what I agree is a worse implementation. Like you, I've spent probably thousands of hours in my career debugging bizarre Python async internal machinery and writing tools that shave off some of the rough edges, and you're right that it's bad out there.
I think the main things that cripple the Python stdlib/asyncio design and the ecosystem that arose around it are:
1. Thread:loop locality. The design of event loop-per-thread is ... admirably flexible, but doomed to smack into the reality that many folks aren't in control of what threads their code is invoked on, and what may or may not have kept a reference to an event loop, which brings me to...
2. The fact that event loop objects can be replaced/started/stopped. That's a terribly confusing API with lots of inherently global state at the edges (open files/pipes used for self-polling, signal handlers), which trips up all sorts of code--including the stdlib itself at times.
A better API design would have, I think, had loops be utility objects without a notion of "running" or not, ideally with the ability to be addressed across threads (note that I'm not asking for coroutines to be auto-scheduled onto multiple threads, just for "asyncio.run"-style entry points and loop objects to work on first access in any thread context). That'd save on the wacky complexity needed for libraries like asgiref (which does a very good job at something that should not be this hard) and reduce "you can't run this coroutine because it's loop is stopped" and "you can't run this coroutine because it's owned by another loop"-type bugs.
Swapping in other loops like uvloop should be a stdlib call that has to happen before any async functionality has run anywhere in the program, and doing it after that should be an error. Or just ... don't support other loops. Realistically there are only two, and the internal "loop" abstraction is so low-level that making it swappable can only speed up certain other kinds of code; a lot of perf is left on the table behind those ugly and expensive "introspect the loop/task/passed-in-awaitable types and states on every single asyncio function in the stdlib" checks you correctly complain about.
The ability to lapse into/out of "blocks" of async code in a largely-synchronous codebase (or between tests) could have been provided with a few simple utilities that asserted things like "crash if any timers/sockets/etc. are still registered with the event loop" or "give me a weakref-tracked list of all live Future and Task handles that the loop has ever manipulated". A ten-liner I use in most async Python projects I touch is basically "write a wrapper class for concurrent.Future and asyncio.Future which stores a weakref from the Future to any async Task that was active when anything wrote to the Future", which makes "is there anything async-condvar shaped that hasn't been GC'd yet?"-type checks trivial.
3. Cancellation is a tarpit, as you pointed out. I'm a big fan of the ability to observe cancellations (the Nodejs model and the Rust model both leave a lot to be desired here), and it's useful to be able to intercept and take action in response to them within reason. However, modelling them as a regular exception runs afoul of too much "except:"-using code, and the idea of "sheild", uncanceling, and accidentally swallowing cancellations is a poorly designed API. Ideally, cancellation handlers would be used in the same way that signal handlers are used--constant-time, minimally-side-effectful code with a guaranteed jump back to resume cancellation propagation when they're done. I understand what making cancels preventable/handleable enables, and it's indeed valuable ... but it's not worth it compared to the pervasive bugs that emerge around the choice of making them a regular exception. Like, sure, it is technically the programmers' fault for misusing CancelledError, but past a point, it's easier to fix the tool than the users.
4. Future objects (especially concurrent.Future's ability to serve as a sync/async "bridge") are treated as low-level tools users should rarely reach for directly. The stdlib them wraps them with some pretty questionably behaving and poorly performing code. I wish the original APIs around asyncio had made the creation and use of Futures the main language things speak, rather than endless utility methods that end up "turducken wrapping" Futures/tasks internally. Relatedly, it's frankly insane the amount of hoops you have to jump through to customize exception propagation between a pair of Future-ish things when chaining them (if A fails with X, sometimes I want B to fail with the same thing, sometimes I don't. If X is CancelledError, the best-practices tools in the stdlib require creating no fewer than six nested/intertwined futures to express "if A fails with a real error, B should fail; if A is cancelled, B should return/do nothing/whatever").
5. I wish the asyncio stdlib had given us more tools to bridge thread/async computation early on (run_coroutine_threadsafe and and run_in_executor are simultaneously too high- and too low-level). Honestly, a ton of the asyncio stdlib feels like it was written as if some of the held-for-constant-time synchronization locks involved in e.g. "call_soon_threadsafe" evoked more fear in the maintainers than the 8 stack frames of chunky pure-python code involved in doing normal thread-local operations. Unconditionally taking out some usually-uncontended coordination locks whenever communicating between different sync stacks/async coroutines would not break the bank--hell, this is what they did for every data object in the program when they removed the GIL. It's fine. Again, modelling an "event loop" as something fixed and cross-thread without a running/stopped state would make this moot, but here we are.
6. There are a lot of preforking systems in Python, from multiprocessing to gunicorn to mod_python. Async code should have been fork-safe--at least as fork-safe as Python's threads, which are hellishly unsafe to fork in the presence of on paper, but which the GIL and interpreter behavior made fork-safe in practice in a huge number of situations (the evidence of which is that thousands of users didn't even know they were in those situations and things kept working fine).
On the other hand, some things that a lot of people rail about in Python async are, I believe, good ideas that should be emulated:
1. Python's got a giant legacy of synchronous code, and (at least when asyncio was created), didn't have the ability to effectively spread work across threads. Given that, the refusal to make async stuff automatically schedule onto multiple threads when a coroutine blocks was a good idea. Loop blocking was just always going to be more common and frustrating in Python async than in JavaScript (with no synchronous legacy), Java (where everyone's used to their code being plopped onto random thread pools anyway), or Rust (with compiler support for expressing that a chunk of code can be moved between threads or not).
2. The taxonomy of awaitables from regular "async def" (fast, no permanent tracked stack) to Futures to Tasks (promises) is a good design. A good async system needs something in all those niches. I just wish the situations where one was automatically promoted to another were handled better.
3. PYTHONASYNCIODEBUG is very useful.
4. Tricky as they are to work with, async generators are very powerful.
5. The decision to implement asyncio itself on top of the original interpreter's generator machinery was a good one (caveat the flaw in the original generator machinery that StopIteration is a regular exception that users can subclass and raise--just like CancelledError, that was not a good decision).
Things like Trio can definitely help shave off some of the rough edges of Python's async (and the stdlib has adopted structured concurrency too nowadays), but even using them inevitably causes some problems since it's too easy to mix and match asyncio and Trio.
At the end of the day, I think Python did pretty well here: it's a big synchronous language with a million deployment targets adopting an entirely new control flow primitive without changing any invariants. The fact that they did as well as they did without industry backing is due in large part both to careful design and the willingness to compromise/ship and improve on incomplete solutions by folks like Victor Stinner. They deserve props. There's still a lot that could be done better.
I think you are right on every point, which is why we should agree that using python async is a bad decision and anyone starting a new project should not use it. Threads might not be what all the cool LLMs are doing, but I have never spent half of all my work time for a month debugging a threading bug, much less half of my day every day forever like with python async. Anyone who says they don't spend half their time debugging python async internals either doesn't use it or doesn't realize they are spending 500x too much on hosting.
I’m not sure I’d go as far as “always a bad decision”, but Python async is definitely something that shouldn’t be adopted without carefully considering alternatives and being aware of the requisite expertise needed. It’s an advanced and sharp-edged tool, and I wish people forced to use it (e.g. due to the use of libraries that only expose an async API) had better tools to make its limited adoption in an otherwise-sync codebase easier.
Especially in Python, threads and blocking code should definitely be encouraged as a first resort.
Yep, if you are going to make asynchronous programming, you need either a small, plain, and obvious layer that calls insulated synchronous code or some stack with high quality assurances.
The thing that never fails to impress me is when an old timer ham copies a signal that's basically right on the noise floor when all i can hear is static. There was an old chap at the radio club i went to and he just had incredibly well tuned hearing. Felt almost superhuman
I'm trying to disentangle this from
the established / proven / trusted "dx core 4" (ask your local devops person if you don't recognise the name).
I found "initiatives" was added. What does this new initiatives measure bring. Why do we care about otherwise unqualified initiatives, how do i know that doesn't just mean using the other 4 proven measures as cover for pushing pet projects without merit?
I'm super cynical tonight it seems. This is just rubbing me up the wrong way i guess and i can't really put my finger on why.
I know I'm biased but Core 4 (and similar) rub me the wrong way – measuring individual developers as the atomic unit IMO is always meaningless. It's a proxy for organizational health but not directly correlated.
Especially now with AI, what do metrics like "prs/engineer" even mean when you have background agents open/reviewing/releasing PRs without human intervention? what is the right unit for measuring health of the org?
FWIW I write a lot more about "why not existing frameworks" in depth in the full paper.
Initiatives are defined specifically as non-productive, technical leverage-producing initiatives that affect the org's health as a whole and are often left behind. For example, we recently ran an initiative around feature flag cleanups and full rollouts that we tracked religiously during our weekly OpEx review – without which we probably would not have had the same success with that cleanup initiative.
I understand your cynicism with "yet another framework" but (and I know I'm biased) this framework is intimately tied to ops reviews as a mechanism for both measurement and organizational change.
The framework is explicitly titled "Measuring developer productivity with the DX Core 4". Diffs/engineer, even if not at the individual level, is still meant as a proxy for individual throughput. The question they are trying to answer is "how do we make Ganesh X% more productive?"
My argument is that that is the wrong question to be trying to optimize for, regardless of how you measure it. It also does not capture the risk dimensions with AI adoption (and the reduction in humans-in-the-loop) which my paper tries to directly address.
Not a good example i'd say given Python's position as pretty much the ultimate glue language :) You'd more likely keep the python shell (and faster developer iteration speed) and push measured hotspots down into c++/rust/c/whatever.
Incidentally, Whenever i've done this in the past it's had a pleasant side effect of improving architecture. You end up forcing something akin to "push for's down and pull if's up" because crossing the ffi boundary is not free. It can be quite magical, as in leading to comically unbelievably speed ups when you also take advantage of vector intrinsics.
But once you've toppled the house of knowledge work, you'll find you can rebuild it easily because the foundation still exists, and it's the thing missing in this analysis; judgement.
reply