<?xml version="1.0"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">

<channel>
	<title>Planet Python</title>
	<link>http://planetpython.org/</link>
	<language>en</language>
	<description>Planet Python - http://planetpython.org/</description>

<item>
	<title>Graham Dumpleton: Tracing Flask with wrapture</title>
	<guid>https://grahamdumpleton.me/posts/2026/09/tracing-flask-with-wrapture/</guid>
	<link>https://grahamdumpleton.me/posts/2026/09/tracing-flask-with-wrapture/</link>
	<description>&lt;p&gt;For a web application the natural unit of tracing is the request: one HTTP request, its method, path and status, and every observed call made while handling it, as one tree. The &lt;a href=&quot;https://grahamdumpleton.me/posts/2026/09/zero-code-tracing-with-wrapture/&quot;&gt;config file from last time&lt;/a&gt; cannot give you that on its own, and it is worth being clear about why before showing what does.&lt;/p&gt;
&lt;p&gt;A WSGI application looks like any other callable, but it routes the interesting facts around the return value. The status and headers travel through the &lt;code&gt;start_response&lt;/code&gt; callback rather than being returned. The body is an iterable that the server consumes after the call has returned, so a streaming application does most of its work after a call event would already have closed. And when a view raises, the framework catches the exception and turns it into a 500 response before any wrapper on the application ever sees it. A binding on the application callable would record a call that returned an iterable and raised nothing, which is true and useless.&lt;/p&gt;
&lt;h2&gt;The shop behind Flask&lt;/h2&gt;
&lt;p&gt;Here is the shop from the earlier posts behind a small Flask application. A &lt;code&gt;/quote/&amp;lt;item&amp;gt;&lt;/code&gt; route renders a template, a &lt;code&gt;/order&lt;/code&gt; route places an order through the &lt;code&gt;OrderService&lt;/code&gt; from before, and a &lt;code&gt;/health&lt;/code&gt; route exists because every deployed service has one.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from flask import Flask, jsonify, render_template, request

from shop import CardDeclined, OrderService

CATALOG = {&amp;quot;widget&amp;quot;: 25, &amp;quot;gadget&amp;quot;: 120}

app = Flask(&amp;quot;webshop&amp;quot;)
service = OrderService()


@app.get(&amp;quot;/health&amp;quot;)
def health():
    return &amp;quot;ok\n&amp;quot;


@app.get(&amp;quot;/quote/&amp;lt;item&amp;gt;&amp;quot;)
def quote(item):
    price = CATALOG[item]
    return render_template(&amp;quot;quote.html&amp;quot;, item=item, price=price)


@app.post(&amp;quot;/order&amp;quot;)
def order():
    data = request.get_json()
    try:
        charge = service.place(data[&amp;quot;amount&amp;quot;], data[&amp;quot;card&amp;quot;], tenant=data[&amp;quot;tenant&amp;quot;])
    except CardDeclined as exc:
        return jsonify(error=str(exc)), 402
    return jsonify(charge)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nothing in it mentions wrapture. The &lt;code&gt;quote&lt;/code&gt; view will raise a &lt;code&gt;KeyError&lt;/code&gt; for an item that is not in the catalog, which Flask will turn into a 500, and that is the request I most want to see.&lt;/p&gt;
&lt;h2&gt;One entry&lt;/h2&gt;
&lt;p&gt;The Flask knowledge lives in an instrumentation package rather than in the config. With &lt;a href=&quot;https://github.com/GrahamDumpleton/wrapture-instrumentation&quot;&gt;wrapture-instrumentation&lt;/a&gt; installed alongside wrapture, the config gains a single &lt;code&gt;[[instrument]]&lt;/code&gt; entry naming Flask, and keeps the observe entries for the shop's own methods from last time:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;[[instrument]]
name = &amp;quot;flask&amp;quot;

[[observe]]
target = &amp;quot;shop:OrderService&amp;quot;
name = &amp;quot;place&amp;quot;
redact = [&amp;quot;card&amp;quot;]

[[observe]]
target = &amp;quot;shop:Gateway&amp;quot;
name = &amp;quot;charge&amp;quot;
redact = [&amp;quot;card&amp;quot;]

[[observe]]
target = &amp;quot;shop:Ledger&amp;quot;
name = &amp;quot;record&amp;quot;

[[sink]]
type = &amp;quot;printer&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The development server runs under the runner exactly as the script did, with everything after &lt;code&gt;-m flask&lt;/code&gt; belonging to Flask:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ python -m wrapture -m flask --app webshop run --port 5001
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then from another shell, a quote, an order, a declined order and the item that does not exist:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ curl http://127.0.0.1:5001/quote/widget
$ curl -X POST -H 'Content-Type: application/json' \
    -d '{&amp;quot;amount&amp;quot;: 500, &amp;quot;card&amp;quot;: &amp;quot;4111-1111-1111-1111&amp;quot;, &amp;quot;tenant&amp;quot;: &amp;quot;acme&amp;quot;}' \
    http://127.0.0.1:5001/order
$ curl -X POST -H 'Content-Type: application/json' \
    -d '{&amp;quot;amount&amp;quot;: 250, &amp;quot;card&amp;quot;: &amp;quot;4000-0000-0000-0000&amp;quot;, &amp;quot;tenant&amp;quot;: &amp;quot;globex&amp;quot;}' \
    http://127.0.0.1:5001/order
$ curl http://127.0.0.1:5001/quote/missing
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the server's log, interleaved with Flask's own access log lines which I have removed here, each request arrives as one tree. The quote:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /quote/widget (webshop.wsgi_app)
  quote(item='widget')
    flask:render_template(template_name_or_list='quote.html', context='&amp;lt;context&amp;gt;')
    flask:render_template -&amp;gt; '&amp;lt;17 chars&amp;gt;' [1.5ms]
  quote -&amp;gt; '&amp;lt;p&amp;gt;widget: 25&amp;lt;/p&amp;gt;' [1.6ms]
webshop.wsgi_app -&amp;gt; '200 OK' [2.3ms, body 5us over 1 chunk]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The request line opens the tree, the view sits beneath it labelled by its endpoint, the template render sits beneath the view with the template's name and its context masked (it is arbitrary application data, and the render is captured only as its size), and the closing line carries the status as the request's result along with the time to the last byte of the body. The order, with the shop's own methods nesting beneath the view because their bindings fire while the request is in flight:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /order (webshop.wsgi_app)
  order()
    shop:OrderService.place(amount=500, card='&amp;lt;redacted&amp;gt;', tenant='acme')
      shop:Gateway.charge(amount=500, card='&amp;lt;redacted&amp;gt;')
      shop:Gateway.charge -&amp;gt; {'id': 'ch_500', 'amount': 500} [7us]
      shop:Ledger.record(entry={'id': 'ch_500', 'amount': 500})
      shop:Ledger.record -&amp;gt; 'led_ch_500' [4us]
    shop:OrderService.place -&amp;gt; {'id': 'ch_500', 'amount': 500} [205us]
  order -&amp;gt; &amp;lt;Response 29 bytes [200 OK]&amp;gt; [471us]
webshop.wsgi_app -&amp;gt; '200 OK' [922us, body 4us over 1 chunk]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The declined card, where the view caught the exception and answered 402, so the failure is on the gateway and the service but not on the request:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /order (webshop.wsgi_app)
  order()
    shop:OrderService.place(amount=250, card='&amp;lt;redacted&amp;gt;', tenant='globex')
      shop:Gateway.charge(amount=250, card='&amp;lt;redacted&amp;gt;')
      shop:Gateway.charge !! CardDeclined [6us]
    shop:OrderService.place !! CardDeclined [74us]
  order -&amp;gt; (&amp;lt;Response 38 bytes [200 OK]&amp;gt;, 402) [265us]
webshop.wsgi_app -&amp;gt; '402 PAYMENT REQUIRED' [673us, body 4us over 1 chunk]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the one I wanted:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /quote/missing (webshop.wsgi_app)
  quote(item='missing')
  quote !! KeyError [4us]
webshop.wsgi_app -&amp;gt; '500 INTERNAL SERVER ERROR' !! KeyError [3.2ms, body 6us over 1 chunk]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The request line says two things at once. It answered 500, and the &lt;code&gt;KeyError&lt;/code&gt; was the reason. That second half is the part a reader would not guess, because as far as the WSGI middleware recording the request is concerned, the application returned normally. Flask caught the exception on its way out of the view and handed it to &lt;code&gt;handle_exception&lt;/code&gt;, which built the 500 response and returned it, so the request completed with a status and no exception. The only place the failure can be seen is inside that handler, where the exception arrives as an argument, and that is where the instrumentation looks. A binding on &lt;code&gt;handle_exception&lt;/code&gt; notes the exception against the nearest enclosing request event, using the same &lt;code&gt;note_exception()&lt;/code&gt; that the testing series used for a failure the code handled itself, aimed past the handler's own call with &lt;code&gt;current_event(kind=&amp;quot;request&amp;quot;)&lt;/code&gt;. The view's event carries the &lt;code&gt;KeyError&lt;/code&gt; as the exception that escaped it, the request's event carries it as a note, and both show up on their lines because two scopes failed for the same reason.&lt;/p&gt;
&lt;h2&gt;Keeping the noise out&lt;/h2&gt;
&lt;p&gt;Health checks and static assets make up most of the traffic on a lot of services and none of the interest. In the log above every &lt;code&gt;/health&lt;/code&gt; probe printed its own tree, which after a day of a load balancer polling it is most of the file. The instrumentation takes a list of paths not to record:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;[[instrument]]
name = &amp;quot;flask&amp;quot;
ignore_paths = [&amp;quot;/health&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A matching request runs and answers as normal but records nothing at all, and the &amp;quot;at all&amp;quot; matters. Declining the request event alone would leave the view, any lifecycle callbacks and any template render it made on the trace as anonymous roots with no request above them, the same problem &lt;code&gt;tree=True&lt;/code&gt; solved on a plain binding in the &lt;a href=&quot;https://grahamdumpleton.me/posts/2026/09/live-tracing-with-wrapture/&quot;&gt;first post&lt;/a&gt;. The setting silences everything beneath an ignored request for its whole extent, so with it in place the health probe leaves only Flask's access log line behind and the next quote prints as before:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;127.0.0.1 - - [01/Sep/2026 14:51:43] &amp;quot;GET /health HTTP/1.1&amp;quot; 200 -
GET /quote/gadget (webshop.wsgi_app)
  quote(item='gadget')
    flask:render_template(template_name_or_list='quote.html', context='&amp;lt;context&amp;gt;')
    flask:render_template -&amp;gt; '&amp;lt;18 chars&amp;gt;' [1.5ms]
  quote -&amp;gt; '&amp;lt;p&amp;gt;gadget: 120&amp;lt;/p&amp;gt;' [1.7ms]
webshop.wsgi_app -&amp;gt; '200 OK' [2.3ms, body 5us over 1 chunk]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The other switch worth knowing about is &lt;code&gt;lifecycle = false&lt;/code&gt;. Flask extensions register &lt;code&gt;before_request&lt;/code&gt; and &lt;code&gt;after_request&lt;/code&gt; callbacks liberally, for loading users, cleaning up sessions and stamping headers, and the instrumentation observes every one of them by default in the order Flask runs them. For an application with several extensions that is faithful but noisy, and switching it off leaves the callbacks running unobserved.&lt;/p&gt;
&lt;h2&gt;What the instrumentation is&lt;/h2&gt;
&lt;p&gt;There is no magic in the &lt;code&gt;[[instrument]]&lt;/code&gt; entry. It names an &lt;code&gt;Instrumentation&lt;/code&gt; class whose hooks run when Flask is imported, and those hooks apply bindings to three choke points in Flask, using the same bindings as everywhere else. Constructing a &lt;code&gt;Flask&lt;/code&gt; instance installs the recording WSGI middleware on its &lt;code&gt;wsgi_app&lt;/code&gt; attribute, so every application the process creates is covered however it was made, application factories included. Registering a route substitutes an observed version of the view function, since Flask captures views into its dispatch table the moment &lt;code&gt;@app.route&lt;/code&gt; runs, before any binding on the module could have seen them. And &lt;code&gt;handle_exception&lt;/code&gt; gets the binding that notes the failure described above. The &lt;code&gt;flask-app&lt;/code&gt; example in the &lt;a href=&quot;https://github.com/GrahamDumpleton/wrapture/tree/main/examples&quot;&gt;wrapture repository&lt;/a&gt; is that class written out in full, as a local file next to a config, for anyone who wants to do the same for a framework that has no package yet. The packaged version adds the lifecycle callbacks, error handlers, blueprints and template rendering on top.&lt;/p&gt;
&lt;h2&gt;The request as an event&lt;/h2&gt;
&lt;p&gt;Everything the tree shows is on the request event itself, which is what a sink or a test reads. The &lt;code&gt;result&lt;/code&gt; is the status line, so every existing filter and assertion that works on a return value works on a request. The &lt;code&gt;duration&lt;/code&gt; is wall time from the call to the close of the body, time to last byte, with the synchronous phase and the body's own share recorded separately. The HTTP details, method, path, query string with sensitive parameters already masked, scheme, remote address and the bytes actually served, sit in the event's &lt;code&gt;data&lt;/code&gt;, and the instrumentation adds the matched route pattern and endpoint once routing has run, which are the low-cardinality keys a backend groups by. The &lt;a href=&quot;https://wrapture.readthedocs.io/en/latest/wsgi-tracing.html&quot;&gt;WSGI request tracing page&lt;/a&gt; has the full event, the &lt;code&gt;mode=&amp;quot;wsgi&amp;quot;&lt;/code&gt; binding form for applications with no framework package, and the redaction rules; ASGI applications get the same treatment on the &lt;a href=&quot;https://wrapture.readthedocs.io/en/latest/asgi-tracing.html&quot;&gt;page beside it&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;With a request as one tree and timings on every line, the next question is the one every web application eventually asks, which is where the time is going.&lt;/p&gt;</description>
	<pubDate>Wed, 09 Sep 2026 01:39:21 +0000</pubDate>
</item>
<item>
	<title>Graham Dumpleton: Zero-code tracing with wrapture</title>
	<guid>https://grahamdumpleton.me/posts/2026/09/zero-code-tracing-with-wrapture/</guid>
	<link>https://grahamdumpleton.me/posts/2026/09/zero-code-tracing-with-wrapture/</link>
	<description>&lt;p&gt;The &lt;a href=&quot;https://grahamdumpleton.me/posts/2026/09/live-tracing-with-wrapture/&quot;&gt;previous post&lt;/a&gt; traced the shop with three bindings and a sink, all applied from the program's own entry point. That is fine when the program is yours. It is less fine when the application is one you inherited and would rather not touch, when someone else owns the deployment, or when you simply do not want observation code living inside the thing being observed. For all of those the entry point edit is one edit too many.&lt;/p&gt;
&lt;p&gt;The same setup can live in a file next to the project instead, with nothing in the program saying so.&lt;/p&gt;
&lt;h2&gt;The file&lt;/h2&gt;
&lt;p&gt;A &lt;code&gt;wrapture.toml&lt;/code&gt; says what to observe and where the events go. For the shop from last time, with the card number redacted as before, that is one &lt;code&gt;[[observe]]&lt;/code&gt; entry per method and one sink:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;[[observe]]
target = &amp;quot;shop:OrderService&amp;quot;
name = &amp;quot;place&amp;quot;
redact = [&amp;quot;card&amp;quot;]

[[observe]]
target = &amp;quot;shop:Gateway&amp;quot;
name = &amp;quot;charge&amp;quot;
redact = [&amp;quot;card&amp;quot;]

[[observe]]
target = &amp;quot;shop:Ledger&amp;quot;
name = &amp;quot;record&amp;quot;

[[sink]]
type = &amp;quot;printer&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;target&lt;/code&gt; is always an exact module or &lt;code&gt;module:path&lt;/code&gt;, never a pattern, and the members within it come from &lt;code&gt;name&lt;/code&gt; for exact members or &lt;code&gt;match&lt;/code&gt; for a glob over the target's own immediate members. That is deliberate. A pattern's blast radius is one level of one named container, stated on the line above it, so &lt;code&gt;match = &amp;quot;*&amp;quot;&lt;/code&gt; on &lt;code&gt;shop:OrderService&lt;/code&gt; can never accidentally wrap something in another module.&lt;/p&gt;
&lt;p&gt;The program itself is &lt;code&gt;main.py&lt;/code&gt;, and it now contains no mention of wrapture at all:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import orders

orders.run()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;python -m wrapture&lt;/code&gt; runner applies the config and then runs the program as &lt;code&gt;__main__&lt;/code&gt;, the same &lt;code&gt;-m&lt;/code&gt; convention as pdb, cProfile and coverage:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ python -m wrapture main.py
shop:OrderService.place(amount=500, card='&amp;lt;redacted&amp;gt;', tenant='acme')
  shop:Gateway.charge(amount=500, card='&amp;lt;redacted&amp;gt;')
  shop:Gateway.charge -&amp;gt; {'id': 'ch_500', 'amount': 500} [8us]
  shop:Ledger.record(entry={'id': 'ch_500', 'amount': 500})
  shop:Ledger.record -&amp;gt; 'led_ch_500' [7us]
shop:OrderService.place -&amp;gt; {'id': 'ch_500', 'amount': 500} [285us]
shop:OrderService.place(amount=250, card='&amp;lt;redacted&amp;gt;', tenant='globex')
  shop:Gateway.charge(amount=250, card='&amp;lt;redacted&amp;gt;')
  shop:Gateway.charge !! CardDeclined [6us]
shop:OrderService.place !! CardDeclined [90us]
shop:OrderService.place(amount=120, card='&amp;lt;redacted&amp;gt;', tenant='globex')
  shop:Gateway.charge(amount=120, card='&amp;lt;redacted&amp;gt;')
  shop:Gateway.charge -&amp;gt; {'id': 'ch_120', 'amount': 120} [4us]
  shop:Ledger.record(entry={'id': 'ch_120', 'amount': 120})
  shop:Ledger.record -&amp;gt; 'led_ch_120' [4us]
shop:OrderService.place -&amp;gt; {'id': 'ch_120', 'amount': 120} [115us]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is the same trace as before, from a program whose source has not changed. The ordering is what makes it work. The config is applied before the target runs, but applying it imports nothing: each observe entry registers a post-import hook for its target module, and the bindings land at the moment the application itself imports &lt;code&gt;shop&lt;/code&gt;, in the application's own import order. A &lt;code&gt;from shop import OrderService&lt;/code&gt; somewhere in the program still picks up the observed class, because the observation is already in place when that line runs, and the program's import order is never changed by observing it.&lt;/p&gt;
&lt;h2&gt;Keeping the trace&lt;/h2&gt;
&lt;p&gt;A printer is for watching. For a program that runs longer than you are willing to sit and look at it, the sink is a file. Swapping the &lt;code&gt;[[sink]]&lt;/code&gt; entry for a JSON Lines one is the only change:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;[[sink]]
type = &amp;quot;jsonlines&amp;quot;
path = &amp;quot;trace.jsonl&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each completed event is written as one JSON object per line, when the event closes, so every line carries the outcome and the timing. The declined charge from the second order looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;seq&amp;quot;: 5,
  &amp;quot;parent_id&amp;quot;: 4,
  &amp;quot;depth&amp;quot;: 1,
  &amp;quot;kind&amp;quot;: &amp;quot;call&amp;quot;,
  &amp;quot;path&amp;quot;: &amp;quot;shop:Gateway.charge&amp;quot;,
  &amp;quot;thread_id&amp;quot;: 140704287927360,
  &amp;quot;thread_name&amp;quot;: &amp;quot;MainThread&amp;quot;,
  &amp;quot;started&amp;quot;: 1150729.898723529,
  &amp;quot;duration&amp;quot;: 0.000004197005182504654,
  &amp;quot;arguments&amp;quot;: {
    &amp;quot;amount&amp;quot;: 250,
    &amp;quot;card&amp;quot;: &amp;quot;&amp;lt;redacted&amp;gt;&amp;quot;
  },
  &amp;quot;exception&amp;quot;: {
    &amp;quot;type&amp;quot;: &amp;quot;CardDeclined&amp;quot;,
    &amp;quot;message&amp;quot;: &amp;quot;card ending 0000 declined&amp;quot;
  },
  &amp;quot;trace&amp;quot;: {
    &amp;quot;w3c&amp;quot;: {
      &amp;quot;trace_id&amp;quot;: &amp;quot;12cd461196239288a8b50e265b6a0f1a&amp;quot;,
      &amp;quot;sampled&amp;quot;: true
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;seq&lt;/code&gt; and &lt;code&gt;parent_id&lt;/code&gt; fields are enough to rebuild the tree, and a field that is absent means it was not captured, so a call that returned &lt;code&gt;None&lt;/code&gt; and a call whose result was never recorded stay distinguishable. The format is the one that &lt;code&gt;jq&lt;/code&gt;, pandas and most log tooling read directly, which means the questions I would otherwise have scrolled a terminal to answer become one-liners. Every call to the gateway, with what it was given and what came back:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ jq -c 'select(.path | endswith(&amp;quot;Gateway.charge&amp;quot;)) | {seq, arguments, result, exception}' trace.jsonl
{&amp;quot;seq&amp;quot;:2,&amp;quot;arguments&amp;quot;:{&amp;quot;amount&amp;quot;:500,&amp;quot;card&amp;quot;:&amp;quot;&amp;lt;redacted&amp;gt;&amp;quot;},&amp;quot;result&amp;quot;:{&amp;quot;id&amp;quot;:&amp;quot;ch_500&amp;quot;,&amp;quot;amount&amp;quot;:500},&amp;quot;exception&amp;quot;:null}
{&amp;quot;seq&amp;quot;:5,&amp;quot;arguments&amp;quot;:{&amp;quot;amount&amp;quot;:250,&amp;quot;card&amp;quot;:&amp;quot;&amp;lt;redacted&amp;gt;&amp;quot;},&amp;quot;result&amp;quot;:null,&amp;quot;exception&amp;quot;:{&amp;quot;type&amp;quot;:&amp;quot;CardDeclined&amp;quot;,&amp;quot;message&amp;quot;:&amp;quot;card ending 0000 declined&amp;quot;}}
{&amp;quot;seq&amp;quot;:7,&amp;quot;arguments&amp;quot;:{&amp;quot;amount&amp;quot;:120,&amp;quot;card&amp;quot;:&amp;quot;&amp;lt;redacted&amp;gt;&amp;quot;},&amp;quot;result&amp;quot;:{&amp;quot;id&amp;quot;:&amp;quot;ch_120&amp;quot;,&amp;quot;amount&amp;quot;:120},&amp;quot;exception&amp;quot;:null}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And everything that raised, which shows the exception at the gateway and again at the order that let it escape:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ jq -c 'select(.exception) | {path, exception}' trace.jsonl
{&amp;quot;path&amp;quot;:&amp;quot;shop:Gateway.charge&amp;quot;,&amp;quot;exception&amp;quot;:{&amp;quot;type&amp;quot;:&amp;quot;CardDeclined&amp;quot;,&amp;quot;message&amp;quot;:&amp;quot;card ending 0000 declined&amp;quot;}}
{&amp;quot;path&amp;quot;:&amp;quot;shop:OrderService.place&amp;quot;,&amp;quot;exception&amp;quot;:{&amp;quot;type&amp;quot;:&amp;quot;CardDeclined&amp;quot;,&amp;quot;message&amp;quot;:&amp;quot;card ending 0000 declined&amp;quot;}}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two properties make this safe to leave running against something real. The application never waits on the file: lines go onto a bounded queue drained by a background thread, and if the queue fills the line is dropped and counted rather than making the observed call block. And the sink captures values as bounded summaries, so an unserialisable argument becomes a short description rather than an error, and no live object is retained. For a process that runs for days the path can carry a date or time variable and rotate on an interval; the &lt;a href=&quot;https://wrapture.readthedocs.io/en/latest/ad-hoc-tracing.html#output-paths-and-rotation&quot;&gt;output paths section&lt;/a&gt; of the documentation has that. The file is also what the exporters read afterwards, so a trace recorded overnight can be rendered for Perfetto the next morning.&lt;/p&gt;
&lt;h2&gt;No launcher at all&lt;/h2&gt;
&lt;p&gt;The runner still owns the command line, and sometimes that is not available either. A service manager, a container entry point or a WSGI server starts the process and you do not get to put &lt;code&gt;python -m wrapture&lt;/code&gt; in front of it. For that case the same config can be injected at interpreter startup through &lt;a href=&quot;https://github.com/GrahamDumpleton/autowrapt&quot;&gt;autowrapt&lt;/a&gt;, a package of mine from some years ago that exists precisely to run registered code once site initialisation completes. Two opt-ins gate it, both outside wrapture:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ pip install autowrapt
$ AUTOWRAPT_BOOTSTRAP=wrapture python main.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The output is identical to the runner's. Installing autowrapt is what makes interpreter startup do anything at all, and the environment variable names wrapture as the thing to bootstrap. Absent either, the entry in wrapture's package metadata is inert, and wrapture itself has no dependency on autowrapt. Underneath, both doors lead to the same place: the post-import hook machinery in wrapt, which autowrapt was originally built on, is what lets wrapture apply a config to modules that have not been imported yet.&lt;/p&gt;
&lt;p&gt;The positioning matters here. Injection is a development, staging and break-glass tool. The unwritten rule for autowrapt has always been that it is not installed on production systems in normal circumstances, precisely because of what it enables, and that installation gate is the feature. Production tracing is the code-level path from the previous post, or a config applied deliberately by the application at startup. Two consequences follow from the mechanism. A config that is missing, or that cannot be applied, warns and lets the process start untraced, because an error at bootstrap would be fatal to an interpreter that has not even started, and the environment variable reaches every Python process launched under it, not only the one you meant. And the bootstrap imports no application code, so bindings still land as the application imports its own modules.&lt;/p&gt;
&lt;h2&gt;Operating a traced process&lt;/h2&gt;
&lt;p&gt;Once injected, the process is still operable. The bootstrap keeps its record of what was applied on &lt;code&gt;wrapture.bootstrap.applied&lt;/code&gt;, and from a console, a debugger or a signal handler that record answers what is installed and lets you switch it off and on without a restart. Running the shop under &lt;code&gt;python -i&lt;/code&gt; so the interpreter drops to a prompt afterwards:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ AUTOWRAPT_BOOTSTRAP=wrapture python -i main.py
...
&amp;gt;&amp;gt;&amp;gt; import wrapture.bootstrap
&amp;gt;&amp;gt;&amp;gt; applied = wrapture.bootstrap.applied
&amp;gt;&amp;gt;&amp;gt; print(applied.report())
sink: Printer()
applied:
  shop:OrderService.place
  shop:Gateway.charge
  shop:Ledger.record
&amp;gt;&amp;gt;&amp;gt; applied.suspend()
&amp;gt;&amp;gt;&amp;gt; import orders; orders.run()
&amp;gt;&amp;gt;&amp;gt; applied.resume()
&amp;gt;&amp;gt;&amp;gt; orders.run()
shop:OrderService.place(amount=500, card='&amp;lt;redacted&amp;gt;', tenant='acme')
  shop:Gateway.charge(amount=500, card='&amp;lt;redacted&amp;gt;')
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While suspended, the wrappers stay in place and the calls pass straight through, so the second &lt;code&gt;orders.run()&lt;/code&gt; printed nothing; after &lt;code&gt;resume()&lt;/code&gt; the third printed the full trace again. &lt;code&gt;revert()&lt;/code&gt; takes the whole intervention down, restoring the patched locations. The &lt;a href=&quot;https://wrapture.readthedocs.io/en/latest/ad-hoc-tracing.html#configuring-from-a-file&quot;&gt;config section&lt;/a&gt; of the ad-hoc tracing page covers everything the file can say beyond what I have used here, including capturing log messages as events beside the calls, and naming instrumentation that a package ships for a framework.&lt;/p&gt;
&lt;p&gt;That last one is where this goes next, because the shop is not really a program that runs three orders and exits. It is a web application, and a web application has a unit of work that a plain binding cannot see.&lt;/p&gt;</description>
	<pubDate>Wed, 09 Sep 2026 00:00:00 +0000</pubDate>
</item>
<item>
	<title>PyCoder’s Weekly: Issue #751: Profiling, From pandas to Polars, NotImplemented, and More (2026-09-08)</title>
	<guid>https://pycoders.com/issues/751</guid>
	<link>https://pycoders.com/issues/751</link>
	<description>&lt;p&gt; &lt;span&gt;#751 – SEPTEMBER 8, 2026&lt;/span&gt;&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/issues/751/feed&quot;&gt;View in Browser »&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;p&gt;&lt;a href=&quot;https://pycoders.com&quot;&gt;&lt;img alt=&quot;The PyCoder&amp;rsquo;s Weekly Logo&quot; src=&quot;https://cdn.pycoders.com/37bdf31dc645f968ffb90196e5d38ff5&quot; /&gt;&lt;/a&gt;&lt;/p&gt; &lt;hr /&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/17017/feed&quot; target=&quot;_blank&quot;&gt;Profiling and Making Apps Fast by Default&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; How do you plan for the performance of your Python applications? What does a performance budget entail, and where should you spend your resources? This week on the show, we speak with Den Odell about his new book &amp;ldquo;Fast by Default: Practical Performance Engineering.&amp;rdquo;&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/17017/feed&quot; target=&quot;_blank&quot;&gt;REAL PYTHON&lt;/a&gt;&lt;/span&gt; &lt;span&gt;podcast&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/17020/feed&quot; target=&quot;_blank&quot;&gt;Migration Strategies for Going From &lt;code&gt;pandas&lt;/code&gt; to &lt;code&gt;Polars&lt;/code&gt;&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; How to scope a pandas to Polars migration, from a single performance-sensitive section to the whole pipeline, and how to execute a full migration by hand or with an LLM.&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/17020/feed&quot; target=&quot;_blank&quot;&gt;THIJS NIEUWDORP&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/17040/feed&quot; target=&quot;_blank&quot;&gt;The Top Open-Source Code Reviewer on Code-Review-Bench&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; PR-AF places #2 of 42 overall on Martian&amp;rsquo;s Code-Review-Bench, ahead of CodeRabbit, Copilot, and Devin. Roughly 3x more valid findings than the commercial tools, at ~10x lower cost per review. Verified findings only. Apache 2.0, self-hosted, runs on any open or closed model. &lt;a href=&quot;https://pycoders.com/link/17040/feed&quot; target=&quot;_blank&quot;&gt;Star &amp;amp; Deploy →&lt;/a&gt;&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/17040/feed&quot; target=&quot;_blank&quot;&gt;AGENTFIELD&lt;/a&gt;&lt;/span&gt; &lt;span&gt;sponsor&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/17022/feed&quot; target=&quot;_blank&quot;&gt;When to Use &lt;code&gt;NotImplemented&lt;/code&gt;&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; When should you return &lt;code&gt;NotImplemented&lt;/code&gt; from a dunder method? Why not return False or raise an exception instead?&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/17022/feed&quot; target=&quot;_blank&quot;&gt;TREY HUNNER&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;h2&gt;Articles &amp;amp; Tutorials&lt;/h2&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/17023/feed&quot; target=&quot;_blank&quot;&gt;Build a Plugin Architecture With a Pydantic and FastAPI&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; Learn how to build a plugin architecture across service boundaries using a shared Pydantic API contract. This article walks through registration-time validation, FastAPI endpoints, ownership and authorization decisions, and continuous health checks for independently deployed services.&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/17023/feed&quot; target=&quot;_blank&quot;&gt;PATRICKM.DE&lt;/a&gt; • Shared by &lt;a href=&quot;https://pycoders.com/link/17033/feed&quot; target=&quot;_blank&quot;&gt;Patrick Müller&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/17028/feed&quot; target=&quot;_blank&quot;&gt;Metadata Requests No Longer Tracked as PyPI Downloads&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; Previously, requests for information about a package on PyPI got counted as a download in the package statistics. This was recently changed to more accurately account only for the downloads of wheels, tar balls, and zip files. This article explains the change.&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/17028/feed&quot; target=&quot;_blank&quot;&gt;PYPI.ORG&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/17041/feed&quot; target=&quot;_blank&quot;&gt;Which AI Tools Are Worth Using? A Live Course for Python Devs With &amp;ldquo;No Time to Try Them Out&amp;rdquo;&lt;/a&gt;&lt;/h3&gt; &lt;a href=&quot;https://pycoders.com/link/17041/feed&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;https://cdn.pycoders.com/b459a70869f3d217e3a5825aad849861&quot; alt=&quot;alt&quot; /&gt;&lt;/a&gt; &lt;p&gt; Stop stressing over every new AI coding tool release: in one live session on September 12 you learn which categories are worth it, which to skip, and a 60-second test that settles every launch after that. &lt;a href=&quot;https://pycoders.com/link/17041/feed&quot; target=&quot;_blank&quot;&gt;Reserve Your Spot →&lt;/a&gt;&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/17041/feed&quot; target=&quot;_blank&quot;&gt;REAL PYTHON&lt;/a&gt;&lt;/span&gt; &lt;span&gt;sponsor&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/17021/feed&quot; target=&quot;_blank&quot;&gt;Build Your Own Face Recognition Tool With Python&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; In this tutorial, you&amp;rsquo;ll build your own face recognition command-line tool with Python. You&amp;rsquo;ll learn how to use face detection to identify faces in an image and label them using face recognition. With this knowledge, you can create your own face recognition tool from start to finish!&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/17021/feed&quot; target=&quot;_blank&quot;&gt;REAL PYTHON&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/17015/feed&quot; target=&quot;_blank&quot;&gt;How to Fix &amp;lsquo;NoneType&amp;rsquo; Object Has No Attribute Errors&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; When Python throws AttributeError: &amp;lsquo;NoneType&amp;rsquo; object has no attribute &amp;lsquo;x&amp;rsquo;, it reads like the interpreter is being deliberately unhelpful, but it&amp;rsquo;s actually telling you something precise: a variable you expected to hold an object turned out to be &lt;code&gt;None&lt;/code&gt;.&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/17015/feed&quot; target=&quot;_blank&quot;&gt;SYSTEM CRAFT PRESS&lt;/a&gt; • Shared by Bob Morrison&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/17031/feed&quot; target=&quot;_blank&quot;&gt;Primer on Python Decorators&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; In this tutorial, you&amp;rsquo;ll look at what Python decorators are and how you define and use them. Decorators can make your code more readable and reusable. Come take a look at how decorators work under the hood and practice writing your own decorators.&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/17031/feed&quot; target=&quot;_blank&quot;&gt;REAL PYTHON&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/17039/feed&quot; target=&quot;_blank&quot;&gt;Type Checking Could Be the Guardrail Your Agent Is Missing&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; Coding agents write a lot of Python, and they write it fast. Having your agent call a typechecker can prevent common type bugs creeping in. Pyrefly is an open-source typechecker built in Rust that’s fast enough to keep up with your agent’s inference.&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/17039/feed&quot; target=&quot;_blank&quot;&gt;PYREFLY TEAM&lt;/a&gt;&lt;/span&gt; &lt;span&gt;sponsor&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/17032/feed&quot; target=&quot;_blank&quot;&gt;Testing Async Python Without Losing Your Mind&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; Async Python testing patterns that actually work: event loop scope, async fixture lifecycle, and the specific pytest-asyncio / anyio patterns that break under default assumptions.&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/17032/feed&quot; target=&quot;_blank&quot;&gt;DEV.TO&lt;/a&gt; • Shared by Anonymous&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/17012/feed&quot; target=&quot;_blank&quot;&gt;Storing Django Static and Media Files on Cloudflare R2&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; This tutorial shows how to configure Django to load and serve up static and media files, public and private, via Cloudflare R2 an AWS S3-like cloud storage service.&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/17012/feed&quot; target=&quot;_blank&quot;&gt;NIK TOMAZIC&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/17035/feed&quot; target=&quot;_blank&quot;&gt;Analysis Paralysis Sucks&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; Junior developers don&amp;rsquo;t start because they don&amp;rsquo;t know enough. Senior developers don&amp;rsquo;t start because they know too many things that could go wrong. Both are stuck.&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/17035/feed&quot; target=&quot;_blank&quot;&gt;KEVIN RENSKERS&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/17038/feed&quot; target=&quot;_blank&quot;&gt;Async Programming in Python: From Generators to &lt;code&gt;asyncio&lt;/code&gt;&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; Learn how Python async programming works. Write async functions with async and await, and run slow I/O operations concurrently with asyncio.&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/17038/feed&quot; target=&quot;_blank&quot;&gt;REAL PYTHON&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/17036/feed&quot; target=&quot;_blank&quot;&gt;Why OOP Exists&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; Learn the fundamental principles behind Object Oriented Programming (OOP) and how that connects to the Python syntax for class definition.&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/17036/feed&quot; target=&quot;_blank&quot;&gt;RODRIGO GIRÃO SERRÃO&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/17019/feed&quot; target=&quot;_blank&quot;&gt;Python 3.15 Preview: UTF-8 by Default&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; Preview the Python 3.15 UTF-8 default: see what changes, try it on a pre-release, and keep your file I/O portable across every platform.&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/17019/feed&quot; target=&quot;_blank&quot;&gt;REAL PYTHON&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/17014/feed&quot; target=&quot;_blank&quot;&gt;Quiz: Python 3.15 Preview: UTF-8 by Default&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/17014/feed&quot; target=&quot;_blank&quot;&gt;REAL PYTHON&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/17029/feed&quot; target=&quot;_blank&quot;&gt;Optimal Seating on the Airbus A380&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; Mark analyzes the results from a paper that determined the optimal seating arrangement on an Airbus A380.&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/17029/feed&quot; target=&quot;_blank&quot;&gt;MARK LITWINTSCHIK&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;h2&gt;Projects &amp;amp; Code&lt;/h2&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/17016/feed&quot; target=&quot;_blank&quot;&gt;shedskin: Restricted-Python-to-C++ Transpiler&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/17016/feed&quot; target=&quot;_blank&quot;&gt;GITHUB.COM/SHEDSKIN&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/17027/feed&quot; target=&quot;_blank&quot;&gt;A Browser DOM, in Python!&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/17027/feed&quot; target=&quot;_blank&quot;&gt;GITHUB.COM/BYTEFACE&lt;/a&gt; • Shared by byteface&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/17025/feed&quot; target=&quot;_blank&quot;&gt;pandas-silent-bugs: 182 Examples Where &lt;code&gt;pandas&lt;/code&gt; Is Wrong&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/17025/feed&quot; target=&quot;_blank&quot;&gt;GITHUB.COM/THIBAUDLEPAN77-SVG&lt;/a&gt; • Shared by Thibaud Lepan&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/17034/feed&quot; target=&quot;_blank&quot;&gt;mitti: Next-Gen ASGI Framework&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/17034/feed&quot; target=&quot;_blank&quot;&gt;GITHUB.COM/GRANDIMAM&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/17024/feed&quot; target=&quot;_blank&quot;&gt;jsonstore: Django JSONField Data as Virtual Model Fields&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/17024/feed&quot; target=&quot;_blank&quot;&gt;GITHUB.COM/VIEWFLOW&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;h2&gt;Events&lt;/h2&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/17026/feed&quot; target=&quot;_blank&quot;&gt;Weekly Real Python Office Hours Q&amp;amp;A (Virtual)&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; September 9, 2026&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/17026/feed&quot; target=&quot;_blank&quot;&gt;REALPYTHON.COM&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/17011/feed&quot; target=&quot;_blank&quot;&gt;Python Atlanta&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; September 10 to September 11, 2026&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/17011/feed&quot; target=&quot;_blank&quot;&gt;MEETUP.COM&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/17018/feed&quot; target=&quot;_blank&quot;&gt;PyDay Boyacá 2026&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; September 12 to September 13, 2026&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/17018/feed&quot; target=&quot;_blank&quot;&gt;PYDAY.CO&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/17013/feed&quot; target=&quot;_blank&quot;&gt;DFW Pythoneers 2nd Saturday Teaching Meeting&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; September 12, 2026&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/17013/feed&quot; target=&quot;_blank&quot;&gt;MEETUP.COM&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/17030/feed&quot; target=&quot;_blank&quot;&gt;DjangoCologne&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; September 15, 2026&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/17030/feed&quot; target=&quot;_blank&quot;&gt;MEETUP.COM&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/17037/feed&quot; target=&quot;_blank&quot;&gt;PyCon Cameroon 2026&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; September 17 to September 20, 2026&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/17037/feed&quot; target=&quot;_blank&quot;&gt;PYTHONCAMEROON.ORG&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;hr /&gt; &lt;p&gt;Happy Pythoning!&lt;br /&gt;This was PyCoder&amp;rsquo;s Weekly Issue #751.&lt;br /&gt;&lt;a href=&quot;https://pycoders.com/issues/751/feed&quot;&gt;View in Browser »&lt;/a&gt;&lt;/p&gt; &lt;img src=&quot;https://pycoders.com/issues/751/open/feed&quot; width=&quot;1&quot; height=&quot;1&quot; alt=&quot;alt&quot; /&gt; 
        &lt;hr /&gt;
        &lt;p&gt;&lt;em&gt;[ Subscribe to 🐍 PyCoder&amp;rsquo;s Weekly 💌 – Get the best Python news, articles, and tutorials delivered to your inbox once a week &lt;a href=&quot;https://pycoders.com/?utm_source=pycoders&amp;utm_medium=feed&amp;utm_campaign=footer&quot;&gt;&amp;gt;&amp;gt; Click here to learn more&lt;/a&gt; ]&lt;/em&gt;&lt;/p&gt;</description>
	<pubDate>Tue, 08 Sep 2026 19:30:00 +0000</pubDate>
</item>
<item>
	<title>Django Weblog: Call for volunteers: Fundraising Working Group</title>
	<guid>https://www.djangoproject.com/weblog/2026/sep/08/call-for-volunteers-fundraising-working-group/</guid>
	<link>https://www.djangoproject.com/weblog/2026/sep/08/call-for-volunteers-fundraising-working-group/</link>
	<description>&lt;p&gt;The Django Software Foundation is looking for people to join the &lt;strong&gt;Fundraising Working Group&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This is a particularly interesting time to get involved.&lt;/p&gt;
&lt;p&gt;The DSF has raised its 2026 fundraising goal to &lt;strong&gt;$500,000&lt;/strong&gt;. That funding is what allows us to continue supporting the Django Fellows, Django Girls, community events, Djangonaut Space, infrastructure, and the many other things that keep the Django ecosystem going. It also gives the DSF the room to do something new: hire its first Executive Director.&lt;/p&gt;
&lt;p&gt;You can read more about the DSF's fundraising goals for 2026 in &lt;a href=&quot;https://www.djangoproject.com/weblog/2026/jun/10/dsf-2026-fundraising-goals/&quot;&gt;this post&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Getting there will take more than asking people to donate. We need to think about how we build relationships with companies that depend on Django, how we make sponsorships meaningful, how we find new ways for organisations to support the project, and how we communicate the value of investing in Django.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;That is where the Fundraising Working Group comes in.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The DSF is hiring an Executive Director who will bring dedicated, day-to-day leadership to the Foundation, including sponsorship development and partner relationships. The Fundraising Working Group will have an opportunity to work closely with the person in this role as we build out our fundraising efforts.&lt;/p&gt;
&lt;h3 id=&quot;s-who-are-we-looking-for&quot;&gt;Who are we looking for?&lt;/h3&gt;
&lt;p&gt;We'd love to have people who have done this before.&lt;/p&gt;
&lt;p&gt;If you have experience with fundraising, sponsorships, partnerships, business development, sales, donor relationships, or building relationships with companies, there is plenty of scope to put that experience to work. We need people who can help identify opportunities, open doors, develop ideas, and turn them into actual fundraising initiatives.&lt;/p&gt;
&lt;p&gt;But &lt;strong&gt;you don't need to be a fundraising expert to join&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Maybe you've never worked on fundraising before, but you know how companies make decisions about supporting open source. Maybe you have ideas for how Django could engage organisations that rely on it. Maybe you are good at building relationships, telling a compelling story, organising initiatives, or simply getting things moving.&lt;/p&gt;
&lt;p&gt;Those perspectives are useful too.&lt;/p&gt;
&lt;p&gt;We're looking for a group that can bring both &lt;strong&gt;experience and fresh ideas&lt;/strong&gt;; people who can help drive the work as well as people who are excited to learn and contribute.&lt;/p&gt;
&lt;p&gt;The working group meets monthly and works asynchronously between meetings. You can read more about how the group operates in the &lt;a href=&quot;https://github.com/django/dsf-working-groups/blob/main/active/fundraising.md&quot;&gt;Fundraising Working Group charter&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Interested in joining?&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://forms.gle/JEKAUYTbcnGQCwFk9&quot;&gt;&lt;strong&gt;Apply to join the Fundraising Working Group&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Whether you have years of fundraising experience or are completely new to it but ready to help, we would love to hear from you.&lt;/p&gt;</description>
	<pubDate>Tue, 08 Sep 2026 19:27:14 +0000</pubDate>
</item>
<item>
	<title>Python Bytes: #495 Banned</title>
	<guid>https://pythonbytes.fm/episodes/show/495/banned</guid>
	<link>https://pythonbytes.fm/episodes/show/495/banned</link>
	<description>&amp;lt;strong&amp;gt;Topics covered in this episode:&amp;lt;/strong&amp;gt;&amp;lt;br&amp;gt;

&amp;lt;ul&amp;gt;
	&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;&amp;lt;a href=&quot;https://www.youtube.com/playlist?list=PLd3Y9yzyC5Uo&quot;&amp;gt;EuroPython 2026 videos are online&amp;lt;/a&amp;gt;&amp;lt;/strong&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;&amp;lt;a href=&quot;https://blog.jetbrains.com/pycharm/2026/08/the-state-of-django-2026-boring-is-so-back/?featured_on=pythonbytes&quot;&amp;gt;The State of Django 2026: Boring is so back&amp;lt;/a&amp;gt;&amp;lt;/strong&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;&amp;lt;a href=&quot;https://four.htmx.org/announcements/2026-08-28-htmx-4.0.0-is-released?featured_on=pythonbytes&quot;&amp;gt;htmx 4.0.0 has been released&amp;lt;/a&amp;gt;&amp;lt;/strong&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;🐍 &amp;lt;a href=&quot;https://testdouble.com/insights/functionally-zen?featured_on=pythonbytes&quot;&amp;gt;Functionally Zen&amp;lt;/a&amp;gt;&amp;lt;/strong&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Extras&amp;lt;/strong&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Joke&amp;lt;/strong&amp;gt;&amp;lt;/li&amp;gt;

&amp;lt;/ul&amp;gt;&amp;lt;a href='https://www.youtube.com/watch?v=yP5MY1R00LU' style='font-weight: bold;'data-umami-event=&quot;Livestream-Past&quot; data-umami-event-episode=&quot;495&quot;&amp;gt;Watch on YouTube&amp;lt;/a&amp;gt;&amp;lt;br&amp;gt;

&amp;lt;p&amp;gt;Sponsored by us! Support our work through:&amp;lt;/p&amp;gt;

&amp;lt;ul&amp;gt;
&amp;lt;li&amp;gt;Our &amp;lt;strong&amp;gt;courses at Talk Python&amp;lt;/strong&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;Consulting from &amp;lt;strong&amp;gt;Six Feet Up&amp;lt;/strong&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Connect with the hosts&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;ul&amp;gt;
&amp;lt;li&amp;gt;Michael: Mastodon / BlueSky / X / LinkedIn&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;Calvin: Mastodon / BlueSky / X / LinkedIn&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;Show: Mastodon / BlueSky / X&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;

&amp;lt;p&amp;gt;Join us on YouTube at &amp;lt;strong&amp;gt;pythonbytes.fm/live&amp;lt;/strong&amp;gt; to be part of the audience. Usually &amp;lt;strong&amp;gt;Tuesday at 7am PT&amp;lt;/strong&amp;gt;. Older video versions available there too.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Finally, if you want an artisanal digest of every week of the show notes in email form? Add your name and email to our friends of the show list, we'll never share it.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Michael #1:&amp;lt;/strong&amp;gt; &amp;lt;a href=&quot;https://www.youtube.com/playlist?list=PLd3Y9yzyC5Uo&quot;&amp;gt;EuroPython 2026 videos are online&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;The EuroPython Society has published all 117 recordings from EuroPython 2026 on the official EuroPython Conference YouTube channel. The conference ran July 13-19 in Krakow, Poland and celebrated the conference series' 25th anniversary. The playlist covers keynotes, panels, lightning talks, and full talk recordings across Python core, web, DevOps, data/ML, embedded, and other tracks.&amp;lt;/p&amp;gt;

&amp;lt;ul&amp;gt;
&amp;lt;li&amp;gt;If you missed EuroPython 2026 in Krakow, this is the complete free on-demand archive of one of the year's biggest European Python events.&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;117 videos&amp;lt;/strong&amp;gt; now live on the EuroPython Conference YouTube channel, last updated Aug 17, 2026.&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;Michael’s personal watch list.&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Calvin #2: &amp;lt;a href=&quot;https://blog.jetbrains.com/pycharm/2026/08/the-state-of-django-2026-boring-is-so-back/?featured_on=pythonbytes&quot;&amp;gt;The State of Django 2026: Boring is so back&amp;lt;/a&amp;gt;&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;ul&amp;gt;
&amp;lt;li&amp;gt;State of Django 2026 (JetBrains/DSF survey, ~3,500 devs, 40+ countries) - &quot;boring is so back&quot;: Django's core stays reliable while everything around it moves fast&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;Core is stable: Postgres 76–79% for 5 years running, templates ~80%, 43% already on Django 6.0&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;AI is routine now (only 10% use none) but workflow's unsettled - Claude Code leads at 35%, and 56% still just use it for chat, not autonomous edits&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;Tooling is consolidating: uv and Ruff both at 43% adoption, each replacing several older single-purpose tools&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;Type hints are winning (57% use them) but the checker is up for grabs - IDE-built-in leads at 40%, Mypy 32%, with ty/Pyrefly emerging&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;Two Django communities coexist happily: 72% server-rendered templates vs. 53% API-only - and htmx adoption jumped from 5% to 34% in five years&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Michael #3:&amp;lt;/strong&amp;gt; &amp;lt;a href=&quot;https://four.htmx.org/announcements/2026-08-28-htmx-4.0.0-is-released?featured_on=pythonbytes&quot;&amp;gt;htmx 4.0.0 has been released&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;After 8 months of work, the htmx team shipped 4.0.0, a rewrite that moves internals from XMLHttpRequest to fetch() while keeping the API almost identical to htmx 2. Three changes may need action: attribute inheritance is now explicit via an :inherited suffix, event names follow a htmx:phase:action pattern, and history no longer caches pages in localStorage. Additions include built-in morph swaps, the new hx-partial tag, and many core extensions. htmx 2 stays supported and remains latest on npm until early 2027.&amp;lt;/p&amp;gt;

&amp;lt;ul&amp;gt;
&amp;lt;li&amp;gt;htmx is the go-to frontend layer for Python server-rendered apps (Flask, Django, FastAPI), and 4.0 is deliberately low-drama: nearly behavior-compatible, so teams can upgrade on their own schedule and pick up morph swaps and streaming extensions.&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;Explicit inheritance is the biggest migration item: hx-confirm, hx-headers, hx-target and friends no longer cascade to children unless you append :inherited; hx-disinherit and hx-inherit are gone&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;A CLI upgrade checker (npx htmx.org@4.0.0 upgrade-check) flags spots needing :inherited, renames like hx-disable to hx-ignore, removed attrs like hx-vars, and old event names in templates and JS&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;Events follow htmx:phase:action (htmx:beforeRequest becomes htmx:before:request); most error events collapse into htmx:error and htmx:xhr:* events are removed with XMLHttpRequest&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;History no longer snapshots pages in localStorage; back navigation re-fetches and swaps into the body, fixing a chronic support headache, with a new hx-history-cache extension for sessionStorage caching&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;New features: out-of-the-box morphing swaps, the [HTML_REMOVED] tag for multi-element updates, streaming over SSE/WebSockets/multipart, and hx-live, their Alpine-inspired DOM scripting solution&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;No forced upgrade: 2.x stays latest on npm until early 2027 (4.0 remains next) and is supported indefinitely; the team even ships official LLM skill files for guidance and upgrading&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Calvin #4: 🐍 &amp;lt;a href=&quot;https://testdouble.com/insights/functionally-zen?featured_on=pythonbytes&quot;&amp;gt;Functionally Zen&amp;lt;/a&amp;gt;&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;ul&amp;gt;
&amp;lt;li&amp;gt;Functionally Zen (Kyle Adams, Test Double) - riffs on &quot;simple is better than complex&quot; with 7 extra tenets for Python simplicity&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;Core claims: idiomatic &amp;amp;gt; non-idiomatic, data &amp;amp;gt; functions, pure functions &amp;amp;gt; impure functions &amp;amp;gt; classes&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;Favorite example: a medical-dosage calculator replaced with a plain lookup dict - no logic, no tests needed&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;Big idea: keep a thin &quot;impure shell&quot; around a &quot;pure core&quot; (Gary Bernhardt's functional core / imperative shell) - push side effects (API calls, DB, files) to the edges&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;Side note: constructors that do I/O are &quot;poison pills&quot; - the side effect infects every class that depends on them&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;Payoff: pure functions and no-mock tests are just easier to read and reason about than the alternative&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Extras&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Calvin&amp;lt;/strong&amp;gt;:&amp;lt;/p&amp;gt;

&amp;lt;ul&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;&amp;lt;a href=&quot;https://github.com/astral-sh/uv/releases/tag/0.12.10?featured_on=pythonbytes&quot;&amp;gt;uv ships trusted-publisher token revocation and Python 3.15 support&amp;lt;/a&amp;gt;&amp;lt;/strong&amp;gt; &amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;a href=&quot;https://austinhenley.com/blog/python1024.html?featured_on=pythonbytes&quot;&amp;gt;Making a Python interpreter in 1024 bytes&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Michael&amp;lt;/strong&amp;gt;:&amp;lt;/p&amp;gt;

&amp;lt;ul&amp;gt;
&amp;lt;li&amp;gt;Steering council voting is now open&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Joke: &amp;lt;a href=&quot;https://www.reddit.com/r/iiiiiiitttttttttttt/comments/1w4j5ka/what_single_word_in_it_makes_you_look_like_this/?share_id=pVKlDEXbUPKdu-UH8fspy&amp;amp;featured_on=pythonbytes&quot;&amp;gt;Makes you look like this?&amp;lt;/a&amp;gt;&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;</description>
	<pubDate>Tue, 08 Sep 2026 18:48:19 +0000</pubDate>
</item>
<item>
	<title>LernerPython blog, from Reuven Lerner: Claude Code always produces something. That’s the hard part.</title>
	<guid>https://lernerpython.com/2026/09/08/claude-code-always-produces-something/</guid>
	<link>https://lernerpython.com/2026/09/08/claude-code-always-produces-something/</link>
	<description>&lt;img width=&quot;683&quot; height=&quot;1024&quot; src=&quot;https://lernerpython.com/wp-content/uploads/2026/03/AI-powered-coding-image-683x1024.png&quot; alt=&quot;&quot; class=&quot;wp-image-13556&quot; /&gt;



&lt;p class=&quot;wp-block-paragraph&quot;&gt;For many months, I&amp;#8217;ve been using Claude Code several hours each day. Not to experiment, but to get work done. A growing amount of the functionality behind &lt;a href=&quot;https://LernerPython.com&quot;&gt;LernerPython.com&lt;/a&gt; is now written with Claude Code: the &lt;a href=&quot;https://lernerpython.com/live-sessions/&quot;&gt;events system&lt;/a&gt;, the membership plumbing, and the scheduled jobs that keep the site&amp;#8217;s session listings current. And, of course, my &lt;a href=&quot;https://practice.lernerpython.com/&quot;&gt;Socratic AI tutor&lt;/a&gt;, which I&amp;#8217;ve not only integrated into my courses, but also into my &amp;#8220;&lt;a href=&quot;https://lernerpython.com/become-a-better-developer/&quot;&gt;Better Developers&lt;/a&gt;&amp;#8221; and &amp;#8220;&lt;a href=&quot;https://BambooWeekly.com&quot;&gt;Bamboo Weekly&lt;/a&gt;&amp;#8221; newsletters. Even my &lt;a href=&quot;https://pypi.org/project/course-setup/&quot;&gt;course-setup&lt;/a&gt; software, which I use several times each week when teaching live sessions, was written with Claude Code.&lt;/p&gt;



&lt;p class=&quot;wp-block-paragraph&quot;&gt;I write less code by hand than I did a year ago, and I ship more of it. Also? I&amp;#8217;m having a blast.&lt;/p&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;The good news: Claude Code always produces something&lt;/h2&gt;



&lt;p class=&quot;wp-block-paragraph&quot;&gt;Long ago, someone told me, &amp;#8220;Computers don&amp;#8217;t do what you want them to do. They do what you tell them to do.&amp;#8221; This was always the case when programming. But with Claude Code, or any agentic coding system, the implications are much bigger. There are gaps between what you wanted to happen, what you specified, and what AI then actually implemented. Just today, Claude told me that gee, it really should have implemented a feature that I had asked for earlier today, and it&amp;#8217;s so sorry that it forgot.&lt;/p&gt;



&lt;p class=&quot;wp-block-paragraph&quot;&gt;Um, you&amp;#8217;re forgiven? I guess?&lt;/p&gt;



&lt;p class=&quot;wp-block-paragraph&quot;&gt;Generally speaking, agents will do what you ask. But you need to understand where and how it might fail, and keep it honest with checklists, follow-up questions, and a very tight development environment. You need to think very carefully not just about the code you want Claude to write, but also about how you can be sure it is really working. Validating your results is, in some ways, more important than the results themselves.&lt;/p&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;What actually changed how I work&lt;/h2&gt;



&lt;p class=&quot;wp-block-paragraph&quot;&gt;Four things, in rough order of how much they&amp;#8217;ve mattered.&lt;/p&gt;



&lt;p class=&quot;wp-block-paragraph&quot;&gt;&lt;strong&gt;A CLAUDE.md file that reads like a contract.&lt;/strong&gt; Mine says to use &lt;code&gt;uv&lt;/code&gt; rather than pip, to add type hints everywhere, to write the test before the implementation, to run &lt;code&gt;ruff&lt;/code&gt; before committing, and to commit small and often. It is not documentation. It&amp;#8217;s the set of standing instructions I got tired of repeating, and every one of those lines was added as soon as something went wrong. It&amp;#8217;s sort of how a company will update its employment contract when they discover something that hadn&amp;#8217;t previously been included, and which let an employee do something they disliked.&lt;/p&gt;



&lt;p class=&quot;wp-block-paragraph&quot;&gt;T&lt;strong&gt;ests, not diffs.&lt;/strong&gt; A test is a claim about behavior, written in a form I can read in ten seconds and disagree with. Test-driven development was always good practice; with an agent in the loop it becomes the primary way you steer. Write the failing test first and the agent has a target it can&amp;#8217;t talk its way around. Ensuring 100% coverage, and adding mutation testing into the mix, make it even less likely that things will go off the rails.&lt;/p&gt;



&lt;p class=&quot;wp-block-paragraph&quot;&gt;&lt;strong&gt;Telemetry and logging, much earlier than I used to add them.&lt;/strong&gt; When code was something I typed, I carried a mental model of it. When code is something I approve, that model is thinner — so I compensate by making the running system easier to observe. I log everything in incredible detail. I get reports e-mailed to me, including self-reflective reports on the AI system itself. I make things visible via APIs, so that I can access and observe information as an administrator. &lt;/p&gt;



&lt;p class=&quot;wp-block-paragraph&quot;&gt;&lt;strong&gt;Small commits as a rollback strategy.&lt;/strong&gt; If every step is its own commit, a session that goes wrong costs you one &lt;code&gt;git revert&lt;/code&gt;, not an afternoon of frustration and debugging. I was always a fan of small commits, and now I&amp;#8217;m even more convinced of their use.&lt;/p&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;Three workshops&lt;/h2&gt;



&lt;p class=&quot;wp-block-paragraph&quot;&gt;I&amp;#8217;m running three Claude Code workshops this month. Each gives you four hours of hands-on work. As usual when I teach, I won&amp;#8217;t use any slides.&lt;/p&gt;



&lt;ul class=&quot;wp-block-list&quot;&gt;
&lt;li&gt;&lt;strong&gt;Wednesday, September 16 — Intro Claude Code with Python.&lt;/strong&gt; Start from the beginning: set Claude Code up properly, then build a command-line utility and a FastAPI app.&lt;/li&gt;



&lt;li&gt;&lt;strong&gt;Thursday, September 17 — Intro Claude Code with Pandas.&lt;/strong&gt; The same starting point, pointed at data: retrieve, clean, analyze and report on real data sets, including inflation and trade data.&lt;/li&gt;



&lt;li&gt;&lt;strong&gt;Wednesday, September 30 — Advanced Claude Code.&lt;/strong&gt; Commands and configuration tricks, plugins and skills (including Superpowers), writing your own skills, using APIs from within Claude Code, and a strong emphasis on testing and telemetry. Plus building a complex data-analysis web app.&lt;/li&gt;
&lt;/ul&gt;



&lt;p class=&quot;wp-block-paragraph&quot;&gt;Each runs 5:30–9:30 p.m. London / 12:30–4:30 p.m. Eastern / 9:30 a.m.–1:30 p.m. Pacific. Each is $300, on top of a LernerPython membership. If you&amp;#8217;re in PythonDAB, all three are included at no extra charge.&lt;/p&gt;



&lt;p class=&quot;wp-block-paragraph&quot;&gt;I&amp;#8217;ve taught versions of this material inside Apple and Cisco, among other companies. If you attended one of my earlier rounds: the exercises are new, and so is a good deal of the material — but I won&amp;#8217;t pretend there&amp;#8217;s zero overlap. The two introductions cover some of the same ground, because they have to. The advanced session revisits a little and then spends most of its time on things I&amp;#8217;ve learned and folded into my own daily work since the spring.&lt;/p&gt;



&lt;p class=&quot;wp-block-paragraph&quot;&gt;&lt;strong&gt;Not sure yet? Come to the free info session on Monday, September 14&lt;/strong&gt;, at 5:30 p.m. London / 12:30 p.m. Eastern / 9:30 a.m. Pacific. It&amp;#8217;s an hour, it&amp;#8217;s free, and you can ask me anything before you decide.&lt;/p&gt;



&lt;p class=&quot;wp-block-paragraph&quot;&gt;&lt;a href=&quot;https://us02web.zoom.us/meeting/register/nnuIxQToQm6a8N2EyutroQ&quot;&gt;Register for the free info session&lt;/a&gt; · &lt;a href=&quot;https://lernerpython.com/code-with-claude/&quot;&gt;Full details and syllabi&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The post &lt;a href=&quot;https://lernerpython.com/2026/09/08/claude-code-always-produces-something/&quot;&gt;Claude Code always produces something. That&amp;#8217;s the hard part.&lt;/a&gt; appeared first on &lt;a href=&quot;https://lernerpython.com&quot;&gt;LernerPython&lt;/a&gt;.&lt;/p&gt;</description>
	<pubDate>Tue, 08 Sep 2026 13:04:49 +0000</pubDate>
</item>
<item>
	<title>Graham Dumpleton: Live tracing with wrapture</title>
	<guid>https://grahamdumpleton.me/posts/2026/09/live-tracing-with-wrapture/</guid>
	<link>https://grahamdumpleton.me/posts/2026/09/live-tracing-with-wrapture/</link>
	<description>&lt;p&gt;When I wrote about &lt;a href=&quot;https://grahamdumpleton.me/posts/2026/09/unit-testing-with-wrapture/&quot;&gt;unit testing with wrapture&lt;/a&gt; the pattern in every test was the same: create a binding on a method, open a &lt;code&gt;timeline()&lt;/code&gt;, run the code, and read the recorded calls off the tape. What I did not say at the time is that nothing about a binding is specific to testing. A binding observes a call site and emits events, and what happens to those events is decided by whoever is listening. In a test the listener is a tape. Take the tape away and register something else, and the same binding narrates a running program as it goes.&lt;/p&gt;
&lt;p&gt;That is the whole idea behind the tracing side of wrapture, and this post is the minimal version of it: the shop from the testing series, three bindings, and one sink.&lt;/p&gt;
&lt;h2&gt;The shop&lt;/h2&gt;
&lt;p&gt;The code is the order service from the earlier posts, grown just enough to have something worth watching. A card number now travels with the order, the gateway declines cards ending in four zeros, and each order belongs to a tenant.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class CardDeclined(Exception):
    pass


class Gateway:
    def charge(self, amount, card):
        if card.endswith(&amp;quot;0000&amp;quot;):
            raise CardDeclined(f&amp;quot;card ending {card[-4:]} declined&amp;quot;)
        return {&amp;quot;id&amp;quot;: f&amp;quot;ch_{amount}&amp;quot;, &amp;quot;amount&amp;quot;: amount}

    def refund(self, charge_id):
        return {&amp;quot;id&amp;quot;: f&amp;quot;re_{charge_id}&amp;quot;}


class Ledger:
    def record(self, entry):
        return f&amp;quot;led_{entry['id']}&amp;quot;


class Notifier:
    def send(self, message):
        return True


class OrderService:
    def __init__(self, gateway=None, ledger=None, notifier=None):
        self.gateway = Gateway() if gateway is None else gateway
        self.ledger = Ledger() if ledger is None else ledger
        self.notifier = Notifier() if notifier is None else notifier

    def place(self, amount, card, tenant):
        charge = self._take_payment(amount, card)
        try:
            self.ledger.record(charge)
        except Exception:
            self.gateway.refund(charge[&amp;quot;id&amp;quot;])
            raise
        self.notifier.send(f&amp;quot;order {charge['id']} placed&amp;quot;)
        return charge

    def _take_payment(self, amount, card):
        return self.gateway.charge(amount, card)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That lives in &lt;code&gt;shop.py&lt;/code&gt;. A second module, &lt;code&gt;orders.py&lt;/code&gt;, places three orders, one of which will be declined:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from shop import CardDeclined, OrderService

ORDERS = [
    (500, &amp;quot;4111-1111-1111-1111&amp;quot;, &amp;quot;acme&amp;quot;),
    (250, &amp;quot;4000-0000-0000-0000&amp;quot;, &amp;quot;globex&amp;quot;),
    (120, &amp;quot;5555-4444-3333-2222&amp;quot;, &amp;quot;globex&amp;quot;),
]


def run():
    service = OrderService()
    for amount, card, tenant in ORDERS:
        try:
            service.place(amount, card, tenant=tenant)
        except CardDeclined:
            pass
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The question to answer is a simple one. When an order is placed, what actually happens? Which methods run, with what, and what comes back? A log line would answer that only in the places where someone had already thought to add one, and this code has none.&lt;/p&gt;
&lt;h2&gt;Three bindings and a sink&lt;/h2&gt;
&lt;p&gt;The entry point applies a binding to each of the three methods that matter and registers a &lt;code&gt;Printer&lt;/code&gt;, which is the simplest sink wrapture ships: it prints each event to standard error as it happens.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import wrapture

from shop import Gateway, Ledger, OrderService
import orders

wrapture.binding(OrderService, &amp;quot;place&amp;quot;).apply()
wrapture.binding(Gateway, &amp;quot;charge&amp;quot;).apply()
wrapture.binding(Ledger, &amp;quot;record&amp;quot;).apply()

wrapture.add_sink(wrapture.Printer())

orders.run()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is no &lt;code&gt;timeline()&lt;/code&gt; anywhere in that. The bindings are applied for the life of the process, the sink is registered for the life of the process, and events flow from one to the other. Running it, the output is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shop:OrderService.place(amount=500, card='4111-1111-1111-1111', tenant='acme')
  shop:Gateway.charge(amount=500, card='4111-1111-1111-1111')
  shop:Gateway.charge -&amp;gt; {'id': 'ch_500', 'amount': 500} [8us]
  shop:Ledger.record(entry={'id': 'ch_500', 'amount': 500})
  shop:Ledger.record -&amp;gt; 'led_ch_500' [6us]
shop:OrderService.place -&amp;gt; {'id': 'ch_500', 'amount': 500} [239us]
shop:OrderService.place(amount=250, card='4000-0000-0000-0000', tenant='globex')
  shop:Gateway.charge(amount=250, card='4000-0000-0000-0000')
  shop:Gateway.charge !! CardDeclined [5us]
shop:OrderService.place !! CardDeclined [60us]
shop:OrderService.place(amount=120, card='5555-4444-3333-2222', tenant='globex')
  shop:Gateway.charge(amount=120, card='5555-4444-3333-2222')
  shop:Gateway.charge -&amp;gt; {'id': 'ch_120', 'amount': 120} [4us]
  shop:Ledger.record(entry={'id': 'ch_120', 'amount': 120})
  shop:Ledger.record -&amp;gt; 'led_ch_120' [3us]
shop:OrderService.place -&amp;gt; {'id': 'ch_120', 'amount': 120} [104us]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each operation gets a line when it begins, indented by how deeply it is nested, and a closing line with the outcome and how long it took. A &lt;code&gt;-&amp;gt;&lt;/code&gt; marks a return value and &lt;code&gt;!!&lt;/code&gt; marks an exception, so the declined card is visible at a glance, and so is the fact that &lt;code&gt;Ledger.record&lt;/code&gt; never ran for that order. These are the real arguments and the real results, the same &lt;code&gt;-&amp;gt;&lt;/code&gt; and &lt;code&gt;!!&lt;/code&gt; markers that &lt;code&gt;tape.tree()&lt;/code&gt; uses in a test, only arriving live rather than being reconstructed afterwards.&lt;/p&gt;
&lt;p&gt;The first thing I noticed in that output is something the trace should not contain. The card numbers are in it, in full, because the bindings captured the arguments as given. The same &lt;code&gt;redact()&lt;/code&gt; capture policy the testing series used for keeping secrets off a tape works here, since the binding is the same object:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;wrapture.binding(OrderService, &amp;quot;place&amp;quot;, capture=wrapture.redact(&amp;quot;card&amp;quot;)).apply()
wrapture.binding(Gateway, &amp;quot;charge&amp;quot;, capture=wrapture.redact(&amp;quot;card&amp;quot;)).apply()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With that in place the opening lines read &lt;code&gt;card='&amp;lt;redacted&amp;gt;'&lt;/code&gt; and everything else is unchanged. I have left it on for the rest of the post, since a trace that is going to be looked at, streamed to a file, or sent anywhere, is exactly the place a card number should not be.&lt;/p&gt;
&lt;h2&gt;What it costs when nobody is listening&lt;/h2&gt;
&lt;p&gt;The obvious worry about leaving bindings applied in a program is what they cost when nothing is being traced. The recording gate in wrapture is not &amp;quot;is there a timeline&amp;quot; but &amp;quot;is anything listening&amp;quot;. A tape scoped to a test is one kind of listener, a process sink is another, and when neither is present an applied binding constructs no event at all. The wrapped method runs with only wrapt's own dispatch on top, which the documentation puts at about half a microsecond per call on the machine it was measured on. That is what makes it reasonable to bind the interesting methods once, in the entry point, and let the sink decide whether anything is recorded.&lt;/p&gt;
&lt;h2&gt;Seeing less&lt;/h2&gt;
&lt;p&gt;Three orders is a readable trace. Three thousand is not, and the answer is rarely to bind fewer things, because the point of binding the layers is to have them there when a question comes up. The tools for narrowing sit either at the sink or at the binding.&lt;/p&gt;
&lt;p&gt;At the sink, combinators wrap a sink and gate what reaches it. &lt;code&gt;Depth(1, ...)&lt;/code&gt; forwards only the roots of each tree, which turns the trace into one opening and one closing line per order:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;wrapture.add_sink(wrapture.Depth(1, wrapture.Printer()))
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;shop:OrderService.place(amount=500, card='&amp;lt;redacted&amp;gt;', tenant='acme')
shop:OrderService.place -&amp;gt; {'id': 'ch_500', 'amount': 500} [149us]
shop:OrderService.place(amount=250, card='&amp;lt;redacted&amp;gt;', tenant='globex')
shop:OrderService.place !! CardDeclined [33us]
shop:OrderService.place(amount=120, card='&amp;lt;redacted&amp;gt;', tenant='globex')
shop:OrderService.place -&amp;gt; {'id': 'ch_120', 'amount': 120} [46us]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At the binding, &lt;code&gt;when=&lt;/code&gt; takes a predicate that is consulted before any event exists. A falsey answer means no event is constructed, no arguments are captured and nothing is delivered, which is the cheap way to narrow a hot call site. Here it records orders for one tenant only:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def acme_only(instance, args, kwargs):
    return kwargs.get(&amp;quot;tenant&amp;quot;) == &amp;quot;acme&amp;quot;

place = wrapture.binding(OrderService, &amp;quot;place&amp;quot;, when=acme_only,
                         capture=wrapture.redact(&amp;quot;card&amp;quot;)).apply()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Running the three orders again gives this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shop:OrderService.place(amount=500, card='&amp;lt;redacted&amp;gt;', tenant='acme')
  shop:Gateway.charge(amount=500, card='&amp;lt;redacted&amp;gt;')
  shop:Gateway.charge -&amp;gt; {'id': 'ch_500', 'amount': 500} [7us]
  shop:Ledger.record(entry={'id': 'ch_500', 'amount': 500})
  shop:Ledger.record -&amp;gt; 'led_ch_500' [6us]
shop:OrderService.place -&amp;gt; {'id': 'ch_500', 'amount': 500} [245us]
shop:Gateway.charge(amount=250, card='&amp;lt;redacted&amp;gt;')
shop:Gateway.charge !! CardDeclined [5us]
shop:Gateway.charge(amount=120, card='&amp;lt;redacted&amp;gt;')
shop:Gateway.charge -&amp;gt; {'id': 'ch_120', 'amount': 120} [4us]
shop:Ledger.record(entry={'id': 'ch_120', 'amount': 120})
shop:Ledger.record -&amp;gt; 'led_ch_120' [4us]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The globex orders are gone, but their gateway and ledger calls are not. A &lt;code&gt;when=&lt;/code&gt; decline skips exactly one event, the declined operation's own, and whatever records beneath it still records, now with nothing above it, so each inner call turns up as an anonymous root with no &lt;code&gt;place&lt;/code&gt; to explain it. Sometimes that is exactly what you want, since a binding whose only job is to intervene in a call should not silence what runs beneath it. When the intent is &amp;quot;nothing from here down&amp;quot;, &lt;code&gt;tree=True&lt;/code&gt; says so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;place = wrapture.binding(OrderService, &amp;quot;place&amp;quot;, when=acme_only, tree=True,
                         capture=wrapture.redact(&amp;quot;card&amp;quot;)).apply()
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;shop:OrderService.place(amount=500, card='&amp;lt;redacted&amp;gt;', tenant='acme')
  shop:Gateway.charge(amount=500, card='&amp;lt;redacted&amp;gt;')
  shop:Gateway.charge -&amp;gt; {'id': 'ch_500', 'amount': 500} [7us]
  shop:Ledger.record(entry={'id': 'ch_500', 'amount': 500})
  shop:Ledger.record -&amp;gt; 'led_ch_500' [6us]
shop:OrderService.place -&amp;gt; {'id': 'ch_500', 'amount': 500} [251us]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the decline covers the whole extent of the declined operation, and the trace is one tenant's orders and nothing else. The skipped calls are not simply lost, either. Each binding counts the operations it declined on &lt;code&gt;filtered_calls&lt;/code&gt;, and after this run &lt;code&gt;place&lt;/code&gt;, &lt;code&gt;charge&lt;/code&gt; and &lt;code&gt;record&lt;/code&gt; report 2, 2 and 1 respectively (the second globex order raised before reaching the ledger), so a trace shorter than expected can be explained rather than guessed at.&lt;/p&gt;
&lt;h2&gt;Where this leaves things&lt;/h2&gt;
&lt;p&gt;The whole intervention is a few lines in the program's entry point: bind the methods that matter, register a sink, and the program describes what it is doing as it runs, with real arguments and real results, and costs next to nothing when nothing is listening. The sink protocol itself is three notifications, so a sink that counts, samples, filters, or writes somewhere of your own is a small class, and the &lt;a href=&quot;https://wrapture.readthedocs.io/en/latest/ad-hoc-tracing.html&quot;&gt;ad-hoc tracing page&lt;/a&gt; of the documentation covers that side, along with the other combinators and the collectors that keep numbers rather than events.&lt;/p&gt;
&lt;p&gt;Those few lines in the entry point are still lines in the program, though. For code you cannot or would rather not edit, they can move out of the program entirely, into a file that sits next to it.&lt;/p&gt;</description>
	<pubDate>Tue, 08 Sep 2026 00:00:00 +0000</pubDate>
</item>
<item>
	<title>Glyph Lefkowitz: ... but what about video games?</title>
	<guid>https://blog.glyph.im/2026/09/but-what-about-video-games.html</guid>
	<link>https://blog.glyph.im/2026/09/but-what-about-video-games.html</link>
	<description>&lt;p&gt;I get asked this rhetorical question a lot, in various forms:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Sure, datacenters might use a lot of energy, but you don’t &lt;em&gt;have&lt;/em&gt; to use a
hosted frontier model to do software development.  What if I just run a local
open-weights model to do some coding, with an open-source coding agent?
Video games also use my GPU.  Is local model development any worse than
playing a video game?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So I want to write down my comprehensive answer to this: Yes, using an LLM to
write some code is worse than playing a video game, for a few reasons.&lt;/p&gt;
&lt;h2 id=&quot;video-games-are-interactive-llms-are-batch-jobs&quot;&gt;Video Games Are Interactive, LLMs Are Batch Jobs&lt;/h2&gt;
&lt;p&gt;Video games use compute to respond to human input. You are using your GPU while
you are looking at a screen, displaying an image. When you are done playing,
you shut off the game, and your computer goes back to idle. It’s much less
energy. By contrast, agentic loops with evals (the only kind of “AI” that is
meaningfully any good at coding) are running hot, for days. To use the most
recent example of such a thing, &lt;a href=&quot;https://forums.paint.net/topic/134563-🍷-extremely-experimental-winelinux-support-how-to-get-started/&quot;&gt;a &lt;em&gt;very rough&lt;/em&gt; first sketch of an
implementation of a Windows graphics API backend to help port a paint program
to other
platforms&lt;/a&gt;,
it took 3 weeks of Claude time, “day and night”. Do you play a lot of video
games for 500 hours to make it past the tutorial level, while &lt;em&gt;also&lt;/em&gt; using
&lt;em&gt;other&lt;/em&gt; computers for other things, as well as the rest of your carbon
footprint?&lt;/p&gt;
&lt;h2 id=&quot;video-games-need-development-llms-need-training&quot;&gt;Video Games Need Development, LLMs Need Training&lt;/h2&gt;
&lt;p&gt;Video games use compute to respond to human input during development, too.
Your game has to be made, but your LLM has to be &lt;em&gt;trained&lt;/em&gt;.  LLMs use a
historically extreme amount of power, &lt;em&gt;probably&lt;/em&gt; using &lt;a href=&quot;https://science.feedback.org/training-and-using-chatgpt-uses-a-lot-of-energy-but-exact-numbers-are-tricky-to-pin-down-without-data-from-openai/&quot;&gt;more than the entire
Internet&lt;/a&gt;,
but it’s kind of hard to say.  Still, it seems a reasonable estimate to within
several orders of magnitude that even over a multi-year project with hundreds
of developers, the power used to develop an individual video game is &lt;em&gt;nowhere
close&lt;/em&gt; to training even a small LLM.&lt;/p&gt;
&lt;p&gt;This is true even for local models.  OpenAI has &lt;a href=&quot;https://www.fdd.org/analysis/2026/02/13/openai-alleges-chinas-deepseek-stole-its-intellectual-property-to-train-its-own-models/&quot;&gt;openly claimed that DeepSeek
“stole its intellectual
property”&lt;/a&gt;,
and I have heard grumblings that none of the open-weights generalist models
could realistically exist without the massive lift that the frontier labs are
doing with their training, in various other ways too.  Secrecy throughout the
industry makes this kind of impossible to understand rigorously, but it seems
fair to say that you are partially culpable for all that famously
energy-intensive frontier lab training if you’re using a local model.&lt;/p&gt;
&lt;h3 id=&quot;and-they-keep-needing-training&quot;&gt;And They &lt;em&gt;Keep&lt;/em&gt; Needing Training&lt;/h3&gt;
&lt;p&gt;You also can’t dismiss this as a sunk cost, because in order to stay current
with industry developments, models need to be updated with new information from
the rest of the world, which means that you need to &lt;em&gt;keep&lt;/em&gt; training them.
Beyond the energy for your own use, if you want a real-life agentic workflow
that actually does useful stuff, practically speaking you would still need to
update your local models over and over again, at least once every few months,
which means you would be incentivizing continued energy consumption by whoever
was doing that training for you, including the energy cost of scraping.&lt;/p&gt;
&lt;h2 id=&quot;lets-be-real-here-you-arent-actually-using-a-local-model&quot;&gt;Let’s Be Real Here, You Aren’t Actually Using A Local Model&lt;/h2&gt;
&lt;p&gt;This question is a hypothetical thought experiment.  Despite &lt;a href=&quot;https://www.faros.ai/blog/open-models-vs-frontier-models&quot;&gt;synthetic
benchmarks that keep showing there isn’t much
difference&lt;/a&gt; between
open weight and frontier models, &lt;a href=&quot;https://aimultiple.com/llm-market-share&quot;&gt;nobody’s actually using local models for much
of anything&lt;/a&gt; beyond sharing those
talking points.  Depending on which benchmark you’re looking at, &lt;a href=&quot;https://whatllm.org/blog/open-source-vs-proprietary-llms-2026&quot;&gt;maybe it’s
good enough or maybe it’s
worse&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;As an inveterate AI hater, all these systems seem pretty bad to me, but it
seems that people who find them useful tend to &lt;em&gt;subjectively&lt;/em&gt; believe the
frontier models are worth the premium, and that’s what they’re actually using.
Once you have accepted that it is OK to use LLMs for coding at all, it seems
like a &lt;em&gt;very&lt;/em&gt; quick slippery slope on down to “we’ll go ahead and use the
frontier models for now anyway, but we could be ethically better in the future
by switching to an open weights one, that option is always available”.&lt;/p&gt;
&lt;h2 id=&quot;theres-a-reason-we-have-data-centers&quot;&gt;There’s A Reason We Have Data Centers&lt;/h2&gt;
&lt;p&gt;Devolving power usage to local LLMs might be good to make users responsible for
their costs and decrease the impacts to communities that are physically next to
huge concentrations of power utilization, not to mention generation.  However,
there’s a reason that it makes sense for the providers to build these giant
facilities: economies of scale &lt;em&gt;reduce&lt;/em&gt; total power consumption, they don’t
increase it.  If you do all the same stuff with a local model that they have to
do in hosted environments, &lt;a href=&quot;https://omniforge.online/blog/green-ai-at-the-edge&quot;&gt;it will probably take &lt;em&gt;more&lt;/em&gt; power, even though you
will be incentivized to do different
stuff&lt;/a&gt;.  This incentive to
“do different stuff” is why although local models can hypothetically hold their
own against the frontier labs for some tasks, when people or businesses take
their inference costs in-house they often find that it’s too painful and move
back to hosted LLMs.&lt;/p&gt;
&lt;h2 id=&quot;there-are-problems-other-than-power&quot;&gt;There Are Problems Other Than Power&lt;/h2&gt;
&lt;p&gt;These are subjects for a different post, but you have to consider a lot of
other externalities: AI psychosis, de-skilling, comprehension debt, cultivating
a dependency, introducing security defects, limiting your design space based on
what LLMs can understand, context rot, wasting time on invalid solutions,
introducing unpredictability into your workflows.  You still have to consider
the &lt;a href=&quot;https://blog.glyph.im/2025/08/futzing-fraction.html&quot;&gt;total cost benefit ratio&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;to-sum-up&quot;&gt;To Sum Up&lt;/h2&gt;
&lt;p&gt;Local LLMs might alleviate &lt;em&gt;some&lt;/em&gt; of the harms from using the hosted frontier
providers.  There are fewer privacy concerns, you can measure your power
utilization and be more directly responsible for it, you can build interfaces
with affordances that are less oriented towards addiction and dependency than
the major frontier labs’ harnesses.&lt;/p&gt;
&lt;p&gt;But they’re not automatically “the same as playing a video game” just because
they can use the same GPU.&lt;/p&gt;
&lt;h2 id=&quot;acknowledgments&quot;&gt;Acknowledgments&lt;/h2&gt;
&lt;p class=&quot;update-note&quot;&gt;Thank you to &lt;a href=&quot;https://glyph.twistedmatrix.com/pages/patrons.html&quot;&gt;my patrons&lt;/a&gt; who are supporting my writing on
this blog.  If you like what you’ve read here and you’d
like to read more of it, or you’d like to support my &lt;a href=&quot;https://github.com/glyph/&quot;&gt;various open-source
endeavors&lt;/a&gt;, you can &lt;a href=&quot;https://glyph.twistedmatrix.com/pages/patrons.html&quot;&gt;support my work as a
sponsor&lt;/a&gt;!&lt;/p&gt;</description>
	<pubDate>Sun, 06 Sep 2026 22:57:00 +0000</pubDate>
</item>
<item>
	<title>The Python Coding Stack: How I Code (Late 2026 Version)</title>
	<guid>https://www.thepythoncodingstack.com/p/how-i-code-late-2026-version</guid>
	<link>https://www.thepythoncodingstack.com/p/how-i-code-late-2026-version</link>
	<description>&lt;p&gt;&lt;span&gt;You open a blank &lt;/span&gt;&lt;code&gt;.py&lt;/code&gt;&lt;span&gt; file in your favourite IDE. You have a blank page in front of you. You start writing code.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;This is how it used to be. And perhaps it&amp;#8217;s how it still is for you. This is how I wrote computer programs just over half a year ago, too. But things started changing gradually for me earlier in 2026.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;Last week, I ran a live course. In the first few minutes of the first live session, I opened a blank &lt;/span&gt;&lt;code&gt;.py&lt;/code&gt;&lt;span&gt; file in my IDE. There was nothing on my screen. Then I wrote code. Word by word. Line by line.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;And about half an hour into this first session, it dawned on me: the last time I had gone through this process of opening a blank file and writing code from scratch was the previous time I had to run a live course a couple of months earlier.&lt;/span&gt;&lt;/p&gt;&lt;h2&gt;&lt;strong&gt;&lt;span&gt;My Agent and I&lt;/span&gt;&lt;/strong&gt;&lt;/h2&gt;&lt;p&gt;&lt;span&gt;Every bit of code I had worked on in between these two live courses was written by my AI agent. I was very much involved in those processes, and I read and reviewed lots of code. But I didn&amp;#8217;t write any.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;Like many, I have mixed feelings about this. I&amp;#8217;m getting a lot more done. But I miss the process of writing code, exploring options, putting a project together, function by function, class by class, module by module.&lt;/span&gt;&lt;/p&gt;&lt;h3&gt;&lt;strong&gt;&lt;span&gt;The Coding Gym&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt;&lt;p&gt;&lt;span&gt;I&amp;#8217;m going to force myself to write code. I&amp;#8217;ll make time for it, as I enjoy it too much and I don&amp;#8217;t want to lose the skills and fluency. But it will be like going for a run or lifting weights in the gym. I don&amp;#8217;t run because I need to get from point A to point B quickly and I don&amp;#8217;t lift weights because I need to move those weights from one place to another. I do those things to keep my body healthy and strong. I&amp;#8217;ll code by hand to do the same thing for my mind.&lt;/span&gt;&lt;/p&gt;&lt;h2&gt;&lt;strong&gt;&lt;span&gt;A Real World Example&lt;/span&gt;&lt;/strong&gt;&lt;/h2&gt;&lt;p&gt;&lt;span&gt;But let&amp;#8217;s go back to how I code now, in late 2026. And let me give you an example of something I worked on last week and used successfully this weekend.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;It&amp;#8217;s not Python code. It&amp;#8217;s a Google Sheets spreadsheet. But that doesn&amp;#8217;t matter. The process is the same.&lt;/span&gt;&lt;/p&gt;&lt;h3&gt;&lt;strong&gt;&lt;span&gt;The Scenario&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt;&lt;p&gt;&lt;span&gt;I&amp;#8217;m a member of an athletics club (or &amp;#8216;track and field&amp;#8217;, depending on which flavour of English you speak). This weekend we had our club championships and I was tasked to take care of results and overall points. We award a shield for the best track performance and another for the best field performance.&lt;/span&gt;&lt;/p&gt;&lt;h3&gt;&lt;strong&gt;&lt;span&gt;The Pain Point&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt;&lt;p&gt;&lt;span&gt;Last year I was asked to help out at the last minute (and by &amp;#8220;last minute&amp;#8221; I mean it almost literally, as it was 20 minutes before the meeting started). It was a nightmare. I wasn&amp;#8217;t responsible for collating the results, that was done by someone else manually as if it was 1965. I just had to work out their performance points by tapping in lots of numbers into an online calculator using my phone. A laptop would have been easier, but I was only tasked with this job on the day and I only had a phone with me.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;This year I had more notice, so I chose to make my life easier... and the whole process smoother.&lt;/span&gt;&lt;/p&gt;&lt;h3&gt;&lt;strong&gt;&lt;span&gt;The Problem&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt;&lt;p&gt;&lt;span&gt;The problem we&amp;#8217;re trying to solve&amp;#8230;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&amp;#8230;is not rocket science. Officials on the track or in the field write results on result sheets and they&amp;#8217;re passed on to the result room by a runner. These sheets contain the name of the event, the athletes&amp;#8217; bib numbers, and their performances. So a result slip may look like this:&lt;/span&gt;&lt;/p&gt;&lt;div class=&quot;highlighted_code_block&quot;&gt;&lt;pre class=&quot;shiki&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;| Event: U16 Boys 100m-1 |                 |
| ---------------------- | --------------- |
| Athlete                | Performance (s) |
| 23                     | 11.5            |
| 47                     | 11.8            |
| 13                     | 11.9            |&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;span&gt;Here&amp;#8217;s what happened last year:&lt;/span&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;span&gt;My colleague had to cross-reference bib numbers with a printed-out sheet of registered athletes. She matched names and age groups, then she sorted results into separate sheets for each event and age group.&lt;/span&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;span&gt;I had to look up each athlete&amp;#8217;s date of birth from a spreadsheet I&amp;#8217;d been given that morning (browsing spreadsheets on a phone screen is not fun), then tap their age, event, and performance into an online calculator, one by one, and write the resulting points on a separate sheet.&lt;/span&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;span&gt;The points allow you to compare performances by athletes of different ages in different events, so that we could then find the single best track performance and the single best field performance.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;I was already thinking, at last year&amp;#8217;s event, of the Python program I could write to automate all of this.&lt;/span&gt;&lt;/p&gt;&lt;h3&gt;&lt;strong&gt;&lt;span&gt;The Solution (I would have never bothered with in 2025)&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt;&lt;p&gt;&lt;span&gt;So, fast-forward to a couple of weeks ago. This time I had some time to come up with something. But I opted to create a Google Sheet instead of a Python program for two reasons:&lt;/span&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;span&gt;It&amp;#8217;s much easier for others in the club to use it and share it in the future.&lt;/span&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;span&gt;My usual objection that it&amp;#8217;s easier and more fun to write a Python program than create a complex spreadsheet full of linked tabs and formulae didn&amp;#8217;t apply. Either way, it was my agent who was going to do the hard work.&lt;/span&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;span&gt;And here&amp;#8217;s the thing. If I was doing this last year, when I was still coding most things manually (and occasionally opening a ChatGPT window to ask a few things), I still wouldn&amp;#8217;t have done this. I wouldn&amp;#8217;t have had the time.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;But this year was different. Sure, if anything I was busier this year than I was last year. But this year I had agents at my fingertips. Agents who know me and have been working alongside me for a while.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;Here&amp;#8217;s why I wouldn&amp;#8217;t have bothered doing this myself:&lt;/span&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;span&gt;The age group cut-off dates are different for younger age groups (31 August), older age groups (31 December), and Masters athletes (over 35s, where the age group is determined on the day of the competition.)&lt;/span&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;span&gt;There are published points tables to compare performances in different events for senior athletes.&lt;/span&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;span&gt;There are published points tables to compare performances in the same event for older athletes, above 30.&lt;/span&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;span&gt;There are older tables to deal with age comparisons for under 30s&lt;/span&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;span&gt;The system needs to take care of all this. None of it is too difficult to write in a Python program or to create one of those power-spreadsheets that link everything together. But it would have required some time and patience, which I didn&amp;#8217;t have.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;But here&amp;#8217;s what I had time for&amp;#8230;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;I was using an agent I communicate with through Discord, which I also have on my phone. I have a good dictation tool on my phone, too. So in the past week, whenever I was preparing dinner, or waiting in line at the shops, or sitting on the sofa in the evening watching TV, I could have a chat with my agent to guide him (it?) to what I wanted. I knew what I wanted. I just didn&amp;#8217;t have the time or desire to do it myself.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;And after a few days of these on-and-off conversations, I had a Google Sheet with 20 tabs, including all the points tables for the various scenarios, all the age group rules, all the registered athletes, who-knows-how-many formulae linking columns, rows, and tabs, and, importantly, just two tabs to input results, one for track events and one for field events.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;You just enter the event session (selecting from a drop-down menu), the bib number, and the performance. And that&amp;#8217;s it. The spreadsheet works out each age group results, the points for each athlete, it shows a live points leaderboard, and so on.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;As I said, none of this is rocket science. I know I would have been able to create this spreadsheet by hand, or write Python code that does the same thing. But I would have needed more time. And I wouldn&amp;#8217;t have been able to multitask as I did last week.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;Oh, and one more thing: I also asked my agent to check the spreadsheet every 15 minutes during our club championships, read the latest results, and post them on a Telegram channel I could share with everyone at the track. So we had live results published online too. I&amp;#8217;d never have bothered to set that up manually.&lt;/span&gt;&lt;/p&gt;&lt;h3&gt;&lt;strong&gt;&lt;span&gt;Programming Will Never Be The Same&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt;&lt;p&gt;&lt;span&gt;The real-world example I described above doesn&amp;#8217;t represent every programming task I work on with the help of my AI agents. This was a hobby-type project that I probably wouldn&amp;#8217;t have worked on otherwise because I didn&amp;#8217;t have the time. It wasn&amp;#8217;t too difficult, but it would have been time-consuming to do by hand. The stakes weren&amp;#8217;t high. Sure, we didn&amp;#8217;t want to make mistakes when assigning medals and shields, but it was easy to spot obvious mistakes, and this wasn&amp;#8217;t the Olympic Games, either!&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;I&amp;#8217;ll write about other case studies soon, including ones where I was more closely involved in the nitty-gritty of the Python code even though I didn&amp;#8217;t write any, nor make any changes to the code by hand.&lt;/span&gt;&lt;/p&gt;&lt;div&gt;&lt;hr /&gt;&lt;/div&gt;&lt;p&gt;&lt;em&gt;&lt;span&gt;Do you want to master Python and programming one article at a time, even in this age of AI? Then don&amp;#8217;t miss out on the articles in The Club which are exclusive to premium subscribers here on The Python Coding Stack&lt;/span&gt;&lt;/em&gt;&lt;/p&gt;&lt;p class=&quot;button-wrapper&quot;&gt;&lt;a class=&quot;button primary&quot; href=&quot;https://www.thepythoncodingstack.com/subscribe&quot;&gt;&lt;span&gt;Subscribe now&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;div&gt;&lt;hr /&gt;&lt;/div&gt;&lt;h3&gt;&lt;strong&gt;&lt;span&gt;Coming Soon&amp;#8230; Exploring SOLID Through AI-Assisted Coding&lt;/span&gt;&lt;/strong&gt;&lt;/h3&gt;&lt;p&gt;&lt;span&gt;I&amp;#8217;m starting a series on the SOLID principles here on &lt;/span&gt;&lt;em&gt;&lt;span&gt;The Python Coding Stack&lt;/span&gt;&lt;/em&gt;&lt;span&gt; soon. I&amp;#8217;ll write posts about each of the five principles, and I&amp;#8217;ll do it my way, the same style I always use when tackling Python topics.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;I&amp;#8217;ll also have a series of posts, which will include video, where I&amp;#8217;ll work on a project from beginning to end using my AI agent and reviewing the code it writes. This project will meander through several OOP concepts, and you&amp;#8217;ll be able to see the SOLID principles come in naturally into the project as solutions to problems we might encounter as the project grows.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;Stay tuned.&lt;/span&gt;&lt;/p&gt;&lt;div&gt;&lt;hr /&gt;&lt;/div&gt;&lt;p&gt;&lt;span&gt;How far are you in the traditional-coding-to-AI-coding arc? Leave a comment and let&amp;#8217;s compare notes!&lt;/span&gt;&lt;/p&gt;&lt;div class=&quot;captioned-image-container&quot;&gt;&lt;a class=&quot;image-link image2 is-viewable-img&quot; target=&quot;_blank&quot; href=&quot;https://substackcdn.com/image/fetch/$s_!wVbn!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F61bacbdb-ec15-431f-abf9-91a5f9515c74_1280x720.png&quot;&gt;&lt;div class=&quot;image2-inset&quot;&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/$s_!wVbn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F61bacbdb-ec15-431f-abf9-91a5f9515c74_1280x720.png&quot; width=&quot;530&quot; height=&quot;298.125&quot; src=&quot;src&quot; /&gt;&lt;div class=&quot;image-link-expand&quot;&gt;&lt;div class=&quot;pencraft pc-display-flex pc-gap-8 pc-reset&quot;&gt;&lt;button tabindex=&quot;0&quot; type=&quot;button&quot; class=&quot;pencraft pc-reset pencraft icon-container restack-image&quot;&gt;&lt;/button&gt;&lt;button tabindex=&quot;0&quot; type=&quot;button&quot; class=&quot;pencraft pc-reset pencraft icon-container view-image&quot;&gt;&lt;/button&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;hr /&gt;&lt;/div&gt;&lt;p&gt;&lt;em&gt;&lt;strong&gt;&lt;a href=&quot;https://www.thepythoncodingstack.com/subscribe&quot;&gt;&lt;span&gt;Join&lt;/span&gt;&lt;/a&gt;&lt;/strong&gt;&lt;/em&gt;&lt;strong&gt;&lt;a href=&quot;https://www.thepythoncodingstack.com/subscribe&quot;&gt;&lt;span&gt; The Club&lt;/span&gt;&lt;/a&gt;&lt;/strong&gt;&lt;em&gt;&lt;span&gt;, the exclusive area for paid subscribers for &lt;/span&gt;&lt;a href=&quot;https://www.thepythoncodingstack.com/s/the-club&quot;&gt;&lt;span&gt;more Python posts&lt;/span&gt;&lt;/a&gt;&lt;span&gt;, videos, a members&amp;#8217; forum, and more.&lt;/span&gt;&lt;/em&gt;&lt;/p&gt;&lt;p class=&quot;button-wrapper&quot;&gt;&lt;a class=&quot;button primary&quot; href=&quot;https://www.thepythoncodingstack.com/subscribe&quot;&gt;&lt;span&gt;Subscribe now&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;&lt;span&gt;You can also support this publication by making a &lt;/span&gt;&lt;a href=&quot;https://buy.stripe.com/00g3de2iGdgg4gg7su&quot;&gt;&lt;span&gt;one-off contribution of any amount you wish&lt;/span&gt;&lt;/a&gt;&lt;span&gt;.&lt;/span&gt;&lt;/em&gt;&lt;/p&gt;&lt;p class=&quot;button-wrapper&quot;&gt;&lt;a class=&quot;button primary&quot; href=&quot;https://buy.stripe.com/00g3de2iGdgg4gg7su&quot;&gt;&lt;span&gt;Support The Python Coding Stack&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;div&gt;&lt;hr /&gt;&lt;/div&gt;&lt;p&gt;&lt;em&gt;&lt;span&gt;For more Python resources, you can also visit&lt;/span&gt;&lt;/em&gt;&lt;span&gt; &lt;/span&gt;&lt;em&gt;&lt;a href=&quot;https://realpython.com/?utm_source=the-python-coding-stack&quot;&gt;&lt;span&gt;Real Python&lt;/span&gt;&lt;/a&gt;&lt;span&gt;&amp;#8212;you may even stumble on one of my own articles or courses there!&lt;/span&gt;&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;&lt;span&gt;Also, are you interested in technical writing? You&amp;#8217;d like to make your own writing more narrative, more engaging, more memorable? Have a look at&lt;/span&gt;&lt;/em&gt;&lt;span&gt; &lt;/span&gt;&lt;em&gt;&lt;a href=&quot;http://stephengruppetta.com/breaking-the-rules&quot;&gt;&lt;span&gt;Breaking the Rules&lt;/span&gt;&lt;/a&gt;&lt;/em&gt;&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;&lt;span&gt;And you can find out more about me at&lt;/span&gt;&lt;/em&gt;&lt;span&gt; &lt;/span&gt;&lt;em&gt;&lt;a href=&quot;https://stephengruppetta.com/?utm_source=the-python-coding-stack&quot;&gt;&lt;span&gt;stephengruppetta.com&lt;/span&gt;&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</description>
	<pubDate>Sun, 06 Sep 2026 20:25:44 +0000</pubDate>
</item>
<item>
	<title>Bob Belderbos: 5 design patterns used in my new habit tracker app</title>
	<guid>https://belderbos.dev/blog/python-patterns-django-habit-tracker/</guid>
	<link>https://belderbos.dev/blog/python-patterns-django-habit-tracker/</link>
	<description>&lt;p&gt;There are plenty of habit trackers, but I wanted to build mine with constraints, a calendar view and habit streaks. So I built &lt;a rel=&quot;noopener external&quot; target=&quot;_blank&quot; href=&quot;https://commitgraph.app&quot;&gt;commitgraph&lt;/a&gt;: a small Django + HTMX app. Here are five Python design and testing patterns from building it.&lt;/p&gt;
&lt;span id=&quot;continue-reading&quot;&gt;&lt;/span&gt;&lt;h2 id=&quot;1-pure-functions&quot;&gt;1. Pure functions&lt;/h2&gt;
&lt;p&gt;Streak counting and calendar shading live in two plain modules. They take dates and return values. And work independently from Django, the database, or the user. That makes them easy to test.&lt;/p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-comment&quot;&gt;# streaks.py&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;from&lt;/span&gt;&lt;span&gt; datetime&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; import&lt;/span&gt;&lt;span&gt; date, timedelta&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-storage z-type&quot;&gt;def&lt;/span&gt;&lt;span class=&quot;z-entity z-name&quot;&gt; longest_streak&lt;/span&gt;&lt;span class=&quot;z-variable z-parameter z-function&quot;&gt;(dates: list[date], is_due&lt;/span&gt;&lt;span class=&quot;z-keyword z-storage z-type&quot;&gt;=lambda&lt;/span&gt;&lt;span class=&quot;z-variable z-parameter z-function&quot;&gt; _:&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt; True&lt;/span&gt;&lt;span&gt;) -&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-support&quot;&gt; int&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    days&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-support&quot;&gt; set&lt;/span&gt;&lt;span&gt;(dates)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;    if not&lt;/span&gt;&lt;span&gt; days:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;        return&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt; 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    best&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; =&lt;/span&gt;&lt;span&gt; run&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt; 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    cur, last&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-support&quot;&gt; min&lt;/span&gt;&lt;span&gt;(days),&lt;/span&gt;&lt;span class=&quot;z-support&quot;&gt; max&lt;/span&gt;&lt;span&gt;(days)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;    while&lt;/span&gt;&lt;span&gt; cur&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; &amp;lt;=&lt;/span&gt;&lt;span&gt; last:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;        if&lt;/span&gt;&lt;span&gt; is_due(cur):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            run&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; =&lt;/span&gt;&lt;span&gt; run&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; +&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt; 1&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; if&lt;/span&gt;&lt;span&gt; cur&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; in&lt;/span&gt;&lt;span&gt; days&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; else&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt; 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            best&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-support&quot;&gt; max&lt;/span&gt;&lt;span&gt;(best, run)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        cur&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; +=&lt;/span&gt;&lt;span&gt; timedelta(&lt;/span&gt;&lt;span class=&quot;z-variable&quot;&gt;days&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;    return&lt;/span&gt;&lt;span&gt; best&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Non-due days don't break the streak, so a Mon/Wed/Fri habit can build a streak across its scheduled days.&lt;/p&gt;
&lt;p&gt;Thanks to this boundary the streak logic is easy to unit test; no database nor user are needed.&lt;/p&gt;
&lt;p&gt;Another example: each calendar cell gets a shade from how much you completed that day. It is tempting to compute that in the template. Instead, I turned it into a helper function:&lt;/p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-comment&quot;&gt;# calendars.py&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-storage z-type&quot;&gt;def&lt;/span&gt;&lt;span class=&quot;z-entity z-name&quot;&gt; shade&lt;/span&gt;&lt;span class=&quot;z-variable z-parameter z-function&quot;&gt;(completed:&lt;/span&gt;&lt;span class=&quot;z-support&quot;&gt; int&lt;/span&gt;&lt;span class=&quot;z-variable z-parameter z-function&quot;&gt;, active:&lt;/span&gt;&lt;span class=&quot;z-support&quot;&gt; int&lt;/span&gt;&lt;span&gt;) -&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-support&quot;&gt; str&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;    if&lt;/span&gt;&lt;span&gt; active&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; ==&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt; 0&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; or&lt;/span&gt;&lt;span&gt; completed&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; ==&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt; 0&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;        return&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt; &amp;quot;bg-[var(--g0)] text-muted&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    frac&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; =&lt;/span&gt;&lt;span&gt; completed&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; /&lt;/span&gt;&lt;span&gt; active&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;    if&lt;/span&gt;&lt;span&gt; frac&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; &amp;gt;=&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt; 1&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;        return&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt; &amp;quot;bg-[#a84420] text-white&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;    if&lt;/span&gt;&lt;span&gt; frac&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; &amp;gt;=&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt; 0.6&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;        return&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt; &amp;quot;bg-[#d97a4e] text-[#3a1c0e]&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;    return&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt; &amp;quot;bg-[#f6ddc9] text-[#3a1c0e]&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;assert shade(5, 5).endswith(&quot;text-white&quot;)&lt;/code&gt; is again easy to test and it's reusable in different parts of the app.&lt;/p&gt;
&lt;h2 id=&quot;2-a-schedule-is-a-7-bit-integer&quot;&gt;2. A schedule is a 7-bit integer&lt;/h2&gt;
&lt;p&gt;Claude suggested this nifty approach: a habit runs on a subset of weekdays. That is seven yes/no answers, which fit cleanly into a single 7-bit integer.&lt;/p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-storage z-type&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;z-entity z-name&quot;&gt; Habit&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-entity&quot;&gt;models&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span class=&quot;z-entity&quot;&gt;Model&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-constant&quot;&gt;    ALL_DAYS&lt;/span&gt;&lt;span class=&quot;z-keyword z-storage z-type&quot;&gt; = 0b&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt;1111111&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-comment&quot;&gt;   # every day&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-constant&quot;&gt;    WEEKDAYS&lt;/span&gt;&lt;span class=&quot;z-keyword z-storage z-type&quot;&gt; = 0b&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt;0011111&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-comment&quot;&gt;   # Mon-Fri (weekday() 0-4)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    due_days&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; =&lt;/span&gt;&lt;span&gt; models.PositiveSmallIntegerField(&lt;/span&gt;&lt;span class=&quot;z-variable&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt;ALL_DAYS&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-storage z-type&quot;&gt;    def&lt;/span&gt;&lt;span class=&quot;z-entity z-name&quot;&gt; is_due_on&lt;/span&gt;&lt;span class=&quot;z-variable z-parameter z-function&quot;&gt;(self, day: date) -&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-support&quot;&gt; bool&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;        return&lt;/span&gt;&lt;span class=&quot;z-support&quot;&gt; bool&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-variable z-language&quot;&gt;self&lt;/span&gt;&lt;span&gt;.due_days&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; &amp;amp;&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; &amp;lt;&amp;lt;&lt;/span&gt;&lt;span&gt; day.weekday()))&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No join table, no seven boolean columns. Adding &quot;weekends only&quot; is a new constant, not a migration.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;is_due_on&lt;/code&gt; one-liner treats &lt;code&gt;due_days&lt;/code&gt; as a &lt;strong&gt;7-bit binary calendar&lt;/strong&gt;, one switch per weekday, ordered right-to-left from Monday (0) to Sunday (6).&lt;/p&gt;
&lt;p&gt;&lt;code&gt;day.weekday()&lt;/code&gt; gives 0-6 (Mon-Sun). &lt;code&gt;1 &amp;lt;&amp;lt; day.weekday()&lt;/code&gt; puts a single bit at that day's position, and &lt;code&gt;&amp;amp; self.due_days&lt;/code&gt; is non-zero only if the habit is scheduled that day. &lt;code&gt;bool()&lt;/code&gt; turns that into &lt;code&gt;True&lt;/code&gt;/&lt;code&gt;False&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;3-let-the-standard-library-build-the-month-grid&quot;&gt;3. Let the standard library build the month grid&lt;/h2&gt;
&lt;p&gt;A month grid can be tricky: leading blanks, trailing blanks, weeks that straddle two months. Python's &lt;code&gt;calendar&lt;/code&gt; module already knows all of it.&lt;/p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;import&lt;/span&gt;&lt;span&gt; calendar&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;for&lt;/span&gt;&lt;span&gt; week&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; in&lt;/span&gt;&lt;span&gt; calendar.Calendar(&lt;/span&gt;&lt;span class=&quot;z-variable&quot;&gt;firstweekday&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt;0&lt;/span&gt;&lt;span&gt;).monthdatescalendar(year, month):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;    for&lt;/span&gt;&lt;span&gt; day&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; in&lt;/span&gt;&lt;span&gt; week:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;        if&lt;/span&gt;&lt;span&gt; day.month&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; !=&lt;/span&gt;&lt;span&gt; month:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-constant&quot;&gt;            ...&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-comment&quot;&gt;  # a padding cell from the previous or next month&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;monthdatescalendar&lt;/code&gt; returns real &lt;code&gt;date&lt;/code&gt; objects, one clean list of weeks; no need to write any logic around how many days April has.&lt;/p&gt;
&lt;h2 id=&quot;4-a-query-vocabulary-on-the-model&quot;&gt;4. A query vocabulary on the model&lt;/h2&gt;
&lt;p&gt;&quot;Active habits&quot; and &quot;habits active on a given day&quot; show up everywhere. Rather than repeating the filters in every view, I named them on a custom &lt;code&gt;QuerySet&lt;/code&gt; so they are chainable:&lt;/p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-storage z-type&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;z-entity z-name&quot;&gt; HabitQuerySet&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-entity&quot;&gt;models&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span class=&quot;z-entity&quot;&gt;QuerySet&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-storage z-type&quot;&gt;    def&lt;/span&gt;&lt;span class=&quot;z-entity z-name&quot;&gt; active&lt;/span&gt;&lt;span class=&quot;z-variable z-parameter z-function&quot;&gt;(self):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;        return&lt;/span&gt;&lt;span class=&quot;z-variable z-language&quot;&gt; self&lt;/span&gt;&lt;span&gt;.filter(&lt;/span&gt;&lt;span class=&quot;z-variable&quot;&gt;archived_at__isnull&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt;True&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-storage z-type&quot;&gt;    def&lt;/span&gt;&lt;span class=&quot;z-entity z-name&quot;&gt; active_on&lt;/span&gt;&lt;span class=&quot;z-variable z-parameter z-function&quot;&gt;(self, day: date):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;        return&lt;/span&gt;&lt;span class=&quot;z-variable z-language&quot;&gt; self&lt;/span&gt;&lt;span&gt;.filter(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            Q(&lt;/span&gt;&lt;span class=&quot;z-variable&quot;&gt;start_date__lte&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt;=&lt;/span&gt;&lt;span&gt;day)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;            &amp;amp;&lt;/span&gt;&lt;span&gt; (Q(&lt;/span&gt;&lt;span class=&quot;z-variable&quot;&gt;archived_at__isnull&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt;True&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; |&lt;/span&gt;&lt;span&gt; Q(&lt;/span&gt;&lt;span class=&quot;z-variable&quot;&gt;archived_at__date__gte&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt;=&lt;/span&gt;&lt;span&gt;day))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-storage z-type&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;z-entity z-name&quot;&gt; Habit&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-entity&quot;&gt;models&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span class=&quot;z-entity&quot;&gt;Model&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    objects&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; =&lt;/span&gt;&lt;span&gt; HabitQuerySet.as_manager()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now &lt;code&gt;Habit.objects.filter(user=u).active()&lt;/code&gt; is very readable and the relatively complex &lt;code&gt;Q&lt;/code&gt; expressions are abstracted into the model.&lt;/p&gt;
&lt;h2 id=&quot;5-done-and-due-is-set-algebra&quot;&gt;5. &quot;Done and due&quot; is set algebra&lt;/h2&gt;
&lt;p&gt;The Today screen needs to know which due habits are already checked off. Two sets and one operator.&lt;/p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;due_ids&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; =&lt;/span&gt;&lt;span&gt; {h.id&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; for&lt;/span&gt;&lt;span&gt; h&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; in&lt;/span&gt;&lt;span&gt; habits}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;done_ids&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-support&quot;&gt; set&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    HabitCompletion.objects&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    .filter(&lt;/span&gt;&lt;span class=&quot;z-variable&quot;&gt;habit__user&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt;=&lt;/span&gt;&lt;span&gt;user,&lt;/span&gt;&lt;span class=&quot;z-variable&quot;&gt; date&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt;=&lt;/span&gt;&lt;span&gt;day)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    .values_list(&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;&amp;quot;habit_id&amp;quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-variable&quot;&gt; flat&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt;True&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; &amp;amp;&lt;/span&gt;&lt;span&gt; due_ids&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The intersection drops any completion for a habit that is not due today, so a spurious record can't inflate the count. The logic is the math, not a nest of &lt;code&gt;if&lt;/code&gt; statements.&lt;/p&gt;
&lt;p&gt;Here is a REPL snippet that demonstrates how set intersection (&lt;code&gt;&amp;amp;&lt;/code&gt;) filters down to only the elements present in both sets:&lt;/p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt; due_ids&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; =&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt;10&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt; 11&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt; 12&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt; 13&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-comment&quot;&gt;      # Habits scheduled for today&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt; completed_ids&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; =&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt;11&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt; 13&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt; 99&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-comment&quot;&gt;    # Logged completions (99 is a stray record)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-comment&quot;&gt; # The '&amp;amp;' operator keeps ONLY IDs present in BOTH sets&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt; done_today&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; =&lt;/span&gt;&lt;span&gt; completed_ids&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; &amp;amp;&lt;/span&gt;&lt;span&gt; due_ids&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-support&quot;&gt; print&lt;/span&gt;&lt;span&gt;(done_today)&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-comment&quot;&gt;  # 10, 12 and 99 are dropped because they are not in both sets&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt;11&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt; 13&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I have also seen this used in RBAC logic to cross-check user roles with endpoint permissions. The intersection of the two sets is the effective permissions a user has.&lt;/p&gt;
&lt;h2 id=&quot;keep-reading&quot;&gt;Keep reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://belderbos.dev/blog/build-the-simplest-thing-that-works/&quot;&gt;Build the Simplest Thing That Works&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://belderbos.dev/blog/unsubscribe-without-login-django-signing/&quot;&gt;Unsubscribe links without a login: Django signing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://belderbos.dev/blog/rust-made-me-a-better-python-developer/&quot;&gt;Learning Rust Made Me a Better Python Developer&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The thread running through all five patterns: when a piece of logic doesn't strictly need the database or a dependency, stick to this boundary. Fetch your primitive values, do the math in plain Python, so it's more testable in isolation.&lt;/p&gt;
&lt;p&gt;What is one function in your Django views that would be much easier to trust and test if you decoupled it?&lt;/p&gt;</description>
	<pubDate>Sun, 06 Sep 2026 00:00:00 +0000</pubDate>
</item>
<item>
	<title>Graham Dumpleton: Beyond callables in wrapture</title>
	<guid>https://grahamdumpleton.me/posts/2026/09/beyond-callables-in-wrapture/</guid>
	<link>https://grahamdumpleton.me/posts/2026/09/beyond-callables-in-wrapture/</link>
	<description>&lt;p&gt;Every example in this series so far has wrapped a call. A binding named a method, and what flowed through the call was recorded or changed. Plenty of what a test needs to control is not a call, though. An outcome stored in an attribute, an environment variable that must be set or missing, a settings dict that other modules imported by reference at import time, a formatter looked up in a registry, and a generator whose interesting behaviour is spread over its consumption. &lt;code&gt;unittest.mock&lt;/code&gt; and pytest between them cover most of this with &lt;code&gt;patch.dict&lt;/code&gt;, &lt;code&gt;monkeypatch.setattr&lt;/code&gt;, &lt;code&gt;monkeypatch.setenv&lt;/code&gt; and so on, one idiom per shape. wrapture spells all of them as bindings, which buys the same lifecycle everywhere, and in a couple of places lets the binding observe as well as hold.&lt;/p&gt;
&lt;h2&gt;Attribute bindings&lt;/h2&gt;
&lt;p&gt;A binding on a class attribute which is not a callable is detected as attribute mode, and instead of &lt;code&gt;on_call&lt;/code&gt; it has &lt;code&gt;on_get&lt;/code&gt;, &lt;code&gt;on_set&lt;/code&gt; and &lt;code&gt;on_delete&lt;/code&gt;, one channel per operation. Under the covers it installs a data descriptor on the class, wrapping whatever was there before, so a property's getter still runs and writes still land in the instance dictionary. Take a model with a status:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class Model:
    status = &amp;quot;draft&amp;quot;

    def publish(self):
        self.status = &amp;quot;published&amp;quot;

    def archive(self):
        self.status = &amp;quot;archived&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inside a timeline, reads and writes record as &lt;code&gt;get&lt;/code&gt; and &lt;code&gt;set&lt;/code&gt; events on the same tape as everything else:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;status = wrapture.binding(Model, &amp;quot;status&amp;quot;)

with wrapture.timeline(status) as tape:
    model = Model()
    model.status
    model.publish()
    model.status

    print(tape.tree())

    status.events.of_kind(&amp;quot;set&amp;quot;).with_value(&amp;quot;published&amp;quot;).assert_once()
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;get __main__:Model.status -&amp;gt; 'draft'
set __main__:Model.status = 'published'
get __main__:Model.status -&amp;gt; 'published'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That assertion says &lt;code&gt;publish()&lt;/code&gt; wrote the status exactly once, without the test knowing anything about how &lt;code&gt;publish()&lt;/code&gt; works inside. A &lt;code&gt;get&lt;/code&gt; event records the value read in &lt;code&gt;result&lt;/code&gt;, the same field a call's return value uses, and a &lt;code&gt;set&lt;/code&gt; event records the value written in &lt;code&gt;value&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The channels carry the same kinds of verb as &lt;code&gt;on_call&lt;/code&gt;. &lt;code&gt;on_get.returns(value)&lt;/code&gt; answers a read without touching the real attribute, &lt;code&gt;on_set.rejects()&lt;/code&gt; makes a write an &lt;code&gt;AttributeError&lt;/code&gt;, &lt;code&gt;on_set.validates(check)&lt;/code&gt; checks a written value and lets it through, and &lt;code&gt;decorates()&lt;/code&gt; takes full control with the real operation handed in as a function. A guard on state transitions, which needs the current value as well as the new one, is a &lt;code&gt;decorates()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;ALLOWED = {(&amp;quot;draft&amp;quot;, &amp;quot;published&amp;quot;), (&amp;quot;published&amp;quot;, &amp;quot;archived&amp;quot;)}

def guard(write, instance, value):
    current = instance.status
    if (current, value) not in ALLOWED:
        raise ValueError(f&amp;quot;cannot move from {current} to {value}&amp;quot;)
    write(value)

status.on_set.decorates(guard)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With that applied, &lt;code&gt;publish()&lt;/code&gt; on a fresh model works, a second &lt;code&gt;publish()&lt;/code&gt; raises &lt;code&gt;cannot move from published to published&lt;/code&gt;, and &lt;code&gt;archive()&lt;/code&gt; then works. The real write happens through &lt;code&gt;write(value)&lt;/code&gt; when the guard allows it. Attribute channels have phases too, so &lt;code&gt;on_get.returns_from([...])&lt;/code&gt; can read one way for two reads and another afterwards.&lt;/p&gt;
&lt;p&gt;Two details come up as soon as this is used on real code. An attribute assigned in &lt;code&gt;__init__&lt;/code&gt; rather than defined on the class does not exist when the binding is created, so the binding takes &lt;code&gt;missing_ok=True&lt;/code&gt;, and the write made in &lt;code&gt;__init__&lt;/code&gt; is then recorded like any other. And when the attribute is a property whose getter does work, the &lt;code&gt;get&lt;/code&gt; event is the parent of whatever that work recorded, which is exactly the question a lazy-loading bug turns on:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class Account:
    def __init__(self):
        self._balance = None

    def load(self):
        return 42

    @property
    def balance(self):
        if self._balance is None:
            self._balance = self.load()
        return self._balance
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;get __main__:Account.balance -&amp;gt; 42
  __main__:Account.load()  -&amp;gt; 42
get __main__:Account.balance -&amp;gt; 42
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first read triggered the load and the second was served from the cache, which is what the property was written to do, and a test can now assert it.&lt;/p&gt;
&lt;p&gt;One limit follows from the mechanism. A descriptor on a class fires for access through instances, so &lt;code&gt;Model.status&lt;/code&gt; read off the class itself returns the descriptor without recording, and a class-level write replaces the descriptor outright, which the binding reports by going inactive rather than pretending. The &lt;a href=&quot;https://wrapture.readthedocs.io/en/latest/known-limitations.html&quot;&gt;known limitations&lt;/a&gt; page has the details.&lt;/p&gt;
&lt;h2&gt;Module attributes&lt;/h2&gt;
&lt;p&gt;A module's plain data is detected as attribute mode too, so a constant or a flag on a module gets the same three channels. A module cannot take a descriptor directly, so while a binding on it is applied the module is given a private subclass of its type with the descriptor installed there, and the original type comes back when the last binding is removed. &lt;code&gt;isinstance(module, ModuleType)&lt;/code&gt; and &lt;code&gt;inspect.ismodule()&lt;/code&gt; are unaffected, and the class is named &lt;code&gt;module&lt;/code&gt; so reprs read the same.&lt;/p&gt;
&lt;p&gt;What is intercepted is access through the module object. Code that did &lt;code&gt;from config import TIMEOUT&lt;/code&gt; at import time holds the value already, and reads through &lt;code&gt;vars(config)&lt;/code&gt; bypass the descriptor, which is the same caveat that applies to patching a module attribute with mock.&lt;/p&gt;
&lt;h2&gt;Value bindings&lt;/h2&gt;
&lt;p&gt;Often a test does not want to observe anything. It wants an environment variable set, a settings key changed, or a module constant lowered, for the duration of the test and then put back. That is a value binding: name the owner positionally, name the slot with &lt;code&gt;attr=&lt;/code&gt; for an attribute or &lt;code&gt;item=&lt;/code&gt; for a mapping entry, and say what it should hold. The pricing function below reads its configuration from all the usual places:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;config.SETTINGS = {&amp;quot;currency&amp;quot;: &amp;quot;USD&amp;quot;, &amp;quot;tax_rate&amp;quot;: 0.2}
config.TIMEOUT = 30.0
config.FORMATTERS = {&amp;quot;plain&amp;quot;: lambda total: f&amp;quot;total={total:.2f}&amp;quot;}

def price(amount, style=&amp;quot;plain&amp;quot;):
    if &amp;quot;API_KEY&amp;quot; not in os.environ:
        raise RuntimeError(&amp;quot;API_KEY is not configured&amp;quot;)
    total = amount * (1 + config.SETTINGS[&amp;quot;tax_rate&amp;quot;])
    formatter = config.FORMATTERS[style]
    return f&amp;quot;[{config.SETTINGS['currency']} within {config.TIMEOUT}s] &amp;quot; + formatter(total)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An environment variable is one entry of &lt;code&gt;os.environ&lt;/code&gt;, so it is &lt;code&gt;item=&lt;/code&gt;. &lt;code&gt;overrides()&lt;/code&gt; holds the value while applied, and on exit the prior state comes back, whether the variable existed before or not:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;api_key = wrapture.binding(os.environ, item=&amp;quot;API_KEY&amp;quot;)

with api_key.overrides(&amp;quot;sk_test&amp;quot;):
    print(price(100))

print(&amp;quot;API_KEY&amp;quot; in os.environ)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;[USD within 30.0s] total=120.00
False
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The other direction is &lt;code&gt;hides()&lt;/code&gt;, under which the slot is absent, which is how the missing-configuration branch gets tested even on a machine where the variable is set. &lt;code&gt;overrides(None)&lt;/code&gt; cannot say that, since &lt;code&gt;None&lt;/code&gt; is a value that is there. A module constant is the same shape with &lt;code&gt;attr=&lt;/code&gt;, and the module can be named by import path so the test needs no import of its own:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;with wrapture.binding(&amp;quot;config&amp;quot;, attr=&amp;quot;TIMEOUT&amp;quot;).overrides(0.5), api_key.overrides(&amp;quot;sk_test&amp;quot;):
    print(price(100))
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;[USD within 0.5s] total=120.00
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A value binding holds a value and observes nothing. It has no channels, no events and no phases, and it says so if you ask for them. The two spellings differ by exactly that: &lt;code&gt;binding(&amp;quot;config&amp;quot;, attr=&amp;quot;TIMEOUT&amp;quot;)&lt;/code&gt; holds, and &lt;code&gt;binding(&amp;quot;config&amp;quot;, &amp;quot;TIMEOUT&amp;quot;)&lt;/code&gt; intercepts. When the question shifts from &amp;quot;hold this value&amp;quot; to &amp;quot;does the retry path re-read the timeout, or did it cache it&amp;quot;, the same location upgrades to the interception form and each read becomes an event:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;timeout = wrapture.binding(&amp;quot;config&amp;quot;, &amp;quot;TIMEOUT&amp;quot;)
timeout.on_get.returns(0.5)

with timeout, wrapture.timeline() as tape, api_key.overrides(&amp;quot;sk_test&amp;quot;):
    price(100)
    price(100)

print([event.kind for event in tape.for_binding(timeout)])
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;['get', 'get']
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two calls, two reads. &lt;code&gt;price()&lt;/code&gt; reads the timeout every time, and the tape proves it.&lt;/p&gt;
&lt;p&gt;Everything around bindings applies to value bindings. They are context managers, they can be suspended and resumed, &lt;code&gt;active&lt;/code&gt; reports whether the slot still holds what the binding put there so a teardown can see that something else overwrote it, and the pytest plugin's leak sweep reports one left applied. In the fixture shape one binding is applied holding nothing and each test says what the slot should be, &lt;code&gt;api_key.overrides(&amp;quot;sk_test&amp;quot;)&lt;/code&gt; in one test and &lt;code&gt;api_key.hides()&lt;/code&gt; in the next.&lt;/p&gt;
&lt;h2&gt;Mapping bindings&lt;/h2&gt;
&lt;p&gt;The settings dict has a complication. Other modules did &lt;code&gt;from config import SETTINGS&lt;/code&gt; at import time, so they hold the same dict by reference, and a test that replaces &lt;code&gt;config.SETTINGS&lt;/code&gt; with a new dict strands them with the old one. &lt;code&gt;mode=&amp;quot;mapping&amp;quot;&lt;/code&gt; on the location mutates the one dict in place and never replaces it, so every holder sees the test's content, and the original entries come back on exit in their original order:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;SETTINGS = config.SETTINGS      # a holder, as another module would have

settings = wrapture.binding(config, &amp;quot;SETTINGS&amp;quot;, mode=&amp;quot;mapping&amp;quot;)

with settings.updates({&amp;quot;tax_rate&amp;quot;: 0.0}), api_key.overrides(&amp;quot;sk_test&amp;quot;):
    print(price(100))

with settings.overrides({&amp;quot;currency&amp;quot;: &amp;quot;EUR&amp;quot;, &amp;quot;tax_rate&amp;quot;: 0.1}), api_key.overrides(&amp;quot;sk_test&amp;quot;):
    print(price(100))

print(SETTINGS, SETTINGS is config.SETTINGS)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;[USD within 30.0s] total=100.00
[EUR within 30.0s] total=110.00
{'currency': 'USD', 'tax_rate': 0.2} True
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;updates()&lt;/code&gt; merges the named keys over what is there, which is &lt;code&gt;patch.dict&lt;/code&gt;'s default, and &lt;code&gt;overrides()&lt;/code&gt; makes the given entries the whole content, which is &lt;code&gt;patch.dict(..., clear=True)&lt;/code&gt;. Both took effect through the holder's reference and both restored it. Three dict spellings exist for three different intentions: &lt;code&gt;item=&lt;/code&gt; for one entry changed or absent, &lt;code&gt;attr=&lt;/code&gt; to make &lt;code&gt;config.SETTINGS&lt;/code&gt; a different object with holders of the old one unaffected, and &lt;code&gt;mode=&amp;quot;mapping&amp;quot;&lt;/code&gt; for the one dict to hold these entries for every holder.&lt;/p&gt;
&lt;p&gt;Bindings group, and a group applies and removes atomically, so a test that needs several of these pinned at once does it in one declaration, and as a fixture the group is a &lt;code&gt;with&lt;/code&gt; around a &lt;code&gt;yield&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;pinned = wrapture.bindings(
    api_key=wrapture.binding(os.environ, item=&amp;quot;API_KEY&amp;quot;).overrides(&amp;quot;sk_test&amp;quot;),
    settings=wrapture.binding(config, &amp;quot;SETTINGS&amp;quot;, mode=&amp;quot;mapping&amp;quot;).overrides({&amp;quot;currency&amp;quot;: &amp;quot;EUR&amp;quot;, &amp;quot;tax_rate&amp;quot;: 0.0}),
    timeout=wrapture.binding(&amp;quot;config&amp;quot;, attr=&amp;quot;TIMEOUT&amp;quot;).overrides(0.5),
)

with pinned:
    print(price(100))
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;[EUR within 0.5s] total=100.00
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;A callable held in a mapping&lt;/h2&gt;
&lt;p&gt;The formatter registry is configuration too, a callable in a dict. A value binding could swap the entry wholesale, but naming the entry with &lt;code&gt;mode=&amp;quot;callable&amp;quot;&lt;/code&gt; wraps it instead. The stand-in is installed in the slot, records like any bound callable, has phases like any bound callable, and the original entry comes back on removal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;loud = wrapture.binding(config.FORMATTERS, item=&amp;quot;plain&amp;quot;, mode=&amp;quot;callable&amp;quot;)
loud.on_call.transforms_result(str.upper)

with loud, api_key.overrides(&amp;quot;sk_test&amp;quot;):
    print(price(100))

print(config.FORMATTERS[&amp;quot;plain&amp;quot;](120.0))
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;[USD within 30.0s] TOTAL=120.00
total=120.00
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The real formatter ran and its result was adjusted on the way out. This reaches a handler in a dispatch table with the whole call vocabulary, which is something that previously needed the callable to be pulled out and wrapped by hand.&lt;/p&gt;
&lt;h2&gt;Generators and iteration&lt;/h2&gt;
&lt;p&gt;A callable that returns a generator produces its values later, one at a time, as the caller iterates. That changes both what recording means and what behaviour can do. Take a paginated catalogue and two consumers, one that reads to the end and one that stops as soon as it finds what it wants:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class Catalogue:
    def __init__(self, records, page_size=2):
        self.records = records
        self.page_size = page_size

    def pages(self, cursor=0):
        while cursor &amp;lt; len(self.records):
            yield {&amp;quot;cursor&amp;quot;: cursor, &amp;quot;items&amp;quot;: self.records[cursor:cursor + self.page_size]}
            cursor += self.page_size


def collect_ids(pages):
    ids = []
    for page in pages:
        ids.extend(item[&amp;quot;id&amp;quot;] for item in page[&amp;quot;items&amp;quot;])
    return ids


def first_match(pages, predicate):
    for page in pages:
        for item in page[&amp;quot;items&amp;quot;]:
            if predicate(item):
                return item
    return None
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A test that hands the consumer a canned list of pages proves it can add up ids and nothing else. A list is never lazy, cannot be abandoned, and cannot fail between items, so the properties a streaming consumer is written to have are exactly the ones such a test cannot check.&lt;/p&gt;
&lt;p&gt;Binding the generator method records one event covering the whole iteration, not one per page, and the event's &lt;code&gt;items&lt;/code&gt; field counts what was pulled through it. Reading to the end fills in &lt;code&gt;result&lt;/code&gt; with the generator's return value, &lt;code&gt;None&lt;/code&gt; here:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;pages = wrapture.binding(Catalogue, &amp;quot;pages&amp;quot;)

with wrapture.timeline(pages) as tape:
    collect_ids(catalogue.pages())
    event = pages.events.first
    print(event.items, event.result)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;3 None
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Stopping early looks different. &lt;code&gt;first_match()&lt;/code&gt; finds id 3 on the second page and returns, dropping the generator before it is exhausted. The event closes with the item count reached and no result at all, &lt;code&gt;wrapture.MISSING&lt;/code&gt; rather than &lt;code&gt;None&lt;/code&gt;, and no &lt;code&gt;-&amp;gt;&lt;/code&gt; in the tree, which is the honest signal that the iteration never finished:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;with wrapture.timeline(pages) as tape:
    first_match(catalogue.pages(), lambda item: item[&amp;quot;id&amp;quot;] == 3)
    event = pages.events.first
    print(event.items, event.result is wrapture.MISSING)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;2 True
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That already answers &amp;quot;how far did it read&amp;quot; and &amp;quot;did it finish&amp;quot; without touching the consumer. Item values are deliberately not captured on the tape, since a long stream would retain every item and no policy can guess which ones matter. When a test wants to see the items, or react to them, it says so with an iterator proxy. &lt;code&gt;iterator()&lt;/code&gt; creates a factory with no target, behaviour is configured on its channels, and calling the factory with a generator returns a wrapped generator applying that behaviour. Since the factory takes an iterator and returns one it slots straight into the binding's &lt;code&gt;transforms_result()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;cursors = []
outcomes = []

watch = wrapture.iterator()
watch.on_item.validates_item(lambda page: cursors.append(page[&amp;quot;cursor&amp;quot;]))
watch.on_finish.validates(lambda value: outcomes.append((&amp;quot;finished&amp;quot;, value)))
watch.on_abandon.notifies(lambda: outcomes.append((&amp;quot;abandoned&amp;quot;, None)))

pages.on_call.transforms_result(watch)

with pages:
    collect_ids(catalogue.pages())
print(cursors, outcomes)

cursors.clear(); outcomes.clear()

with pages:
    first_match(catalogue.pages(), lambda item: item[&amp;quot;id&amp;quot;] == 3)
print(cursors, outcomes)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;[0, 2, 4] [('finished', None)]
[0, 2] [('abandoned', None)]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;on_abandon&lt;/code&gt; fires when a started, unexhausted generator is closed, whether explicitly or because the consumer dropped it and the garbage collector closed it. That is the question nothing else can see asked: the loop that stopped early, the generator left half-consumed. The proxy also has &lt;code&gt;on_error&lt;/code&gt; for an iteration that raised, and &lt;code&gt;on_item.transforms_item()&lt;/code&gt; to rewrite each item on its way through.&lt;/p&gt;
&lt;p&gt;An item stage that raises fails the iteration at that point, as if the generator itself had raised while producing that page, which is how to test what a consumer does when page two fails to arrive:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def fail_at(position, exc):
    seen = 0

    def check(page):
        nonlocal seen
        seen += 1
        if seen == position:
            raise exc

    return check

flaky = wrapture.iterator()
flaky.on_item.validates_item(fail_at(2, OSError(&amp;quot;page 2 failed&amp;quot;)))
pages.on_call.transforms_result(flaky)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With that applied, &lt;code&gt;collect_ids()&lt;/code&gt; receives the first page and then an &lt;code&gt;OSError&lt;/code&gt; on the second, and a consumer written to cope with that can be tested doing so.&lt;/p&gt;
&lt;h2&gt;One lifecycle for all of it&lt;/h2&gt;
&lt;p&gt;The thread through everything here is that whichever shape a patch takes, it is a binding, and everything that applies to a binding applies to it. It is a context manager and it has a decorator form. It groups with other bindings and the group applies and removes as one unit. It can be suspended and resumed, it knows whether it is still in place, and the pytest plugin's leak sweep reports it if a test forgets to remove it. Where the shape allows it, the same binding that holds a value can be upgraded to one that sees who reads it, and a callable pulled out of a dict gets the same phases and recording as one on a class.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://wrapture.readthedocs.io/en/latest/monkey-patching.html&quot;&gt;monkey patching&lt;/a&gt; guide is the full reference for every binding mode, and the worked examples on &lt;a href=&quot;https://wrapture.readthedocs.io/en/latest/example-pinning-configuration.html&quot;&gt;pinning configuration&lt;/a&gt;, &lt;a href=&quot;https://wrapture.readthedocs.io/en/latest/example-resource-hygiene.html&quot;&gt;checking that resources are released&lt;/a&gt; and &lt;a href=&quot;https://wrapture.readthedocs.io/en/latest/example-streaming-data.html&quot;&gt;testing generators and streamed results&lt;/a&gt; each take one of the questions above further than a blog post has room for.&lt;/p&gt;</description>
	<pubDate>Sat, 05 Sep 2026 00:00:00 +0000</pubDate>
</item>
<item>
	<title>Armin Ronacher: Latent Powers</title>
	<guid>https://lucumr.pocoo.org/2026/9/5/latent-powers/</guid>
	<link>https://lucumr.pocoo.org/2026/9/5/latent-powers/</link>
	<description>&lt;p&gt;A few weeks ago I felt like it would be fun to see if I can make one of those
cheap Chinese CarPlay dongles run something other than the stock firmware.  The
idea was that rather than just forwarding CarPlay, why not do something more
interesting with them?  They all work quite similarly: they act as bridges
between your car and the phone.  From there they deal with video and audio
streams and pass some other data through.  Most of them also bring up a custom
UI for pairing and have a web interface that your phone can reach for updates.&lt;/p&gt;
&lt;p&gt;Long story short: I had a conversation with Fable and Sol via Pi about what
could be done with such a dongle or whether I should use a Raspberry Pi instead
if I wanted to do my own thing there.  I figured it might be quite fun to run my
own code while still allowing regular CarPlay to pass through.&lt;/p&gt;
&lt;p&gt;Through working with the LLM I learned about
&lt;a href=&quot;https://github.com/catplay-labs/catplay&quot;&gt;CatPlay&lt;/a&gt;, which is a Rust
reimplementation of the CarPlay protocol that can run on Carlinkit devices.  In
particular, it can run on the Carlinkit Mini Ultra, which I figured would be
easy enough to buy.  I do have a few CarPlay adapters around, but I did not have
that particular model, so I bought one on Amazon.  Twenty-four hours later, I
had a device in my hand that was branded as a Carlinkit Mini Ultra, but instead
of being the Ingenic device that the original author used, it turned out to be
something else.&lt;/p&gt;
&lt;p&gt;This is normally where the story would stop.  However, it&amp;#8217;s 2026.  Armed with a
bit of knowledge about how these systems work, I managed to have some fruitful
discussions with Kimi K3 and Sol and figure out how &lt;a href=&quot;https://github.com/catplay-labs/catplay-firmware/issues/4&quot;&gt;flash the
device&lt;/a&gt; and in turn,
how to make CatPlay compile for that SoC.&lt;/p&gt;
&lt;p&gt;I guess that hacking these USB devices is not necessarily hard, but it&amp;#8217;s
laborious and you can easily end up bricking your devices.  It also just sucks
because sometimes you need to work with someone else&amp;#8217;s code that does not itself
run on your machine.  In the past, I would abandon many such projects for lack
of tenacity.  But my clanker is tenacious.&lt;/p&gt;
&lt;p&gt;But so are &lt;em&gt;all of our clankers&lt;/em&gt;.  Some of the projects we&amp;#8217;re now attempting are
happening because of conversations we have with them.  In this case I did not
find or decide on CatPlay, the model did.  It was not the only suggestion, but
it became the best starting point after discarding others.&lt;/p&gt;
&lt;p&gt;And I discover this more and more.  Particularly when we have solitary
interactions with these models, some of us &amp;#8220;independently&amp;#8221; decide to work on
similar projects.  When I talked with an acquaintance about CarPlay he also
mentioned recently that he decided to try something similar because he too
wanted to see if he can get his own agent be hooked up with the car.  And guess
what: he too learned about the CarPlay hacking community, and that it&amp;#8217;s an
option, from the models and roughly around the same time.&lt;/p&gt;
&lt;p&gt;It really got me thinking about how this could create situations in which
completely independent people end up building things they believe are their own
ideas.  Yet they were inspired or pushed towards doing something by a
conversation with an LLM — a conversation that someone else also had.  What if
we took paths, because those were the paths that were more likely with current
generation models?  There is a running joke in the AI builder community right
now that we&amp;#8217;re all working on the same things, and in many ways it feels like we
are.  That might be because those things are obvious, or it might be partly
because we all use the same models with the same capabilities.&lt;/p&gt;
&lt;p&gt;A few months ago, I first saw &lt;a href=&quot;https://x.com/lucasmeijer&quot;&gt;Lucas Meijer&lt;/a&gt; share the
idea to make a model in Pi produce HTML reports rather than Markdown.  I thought
that was pretty unique.  Except, well turns out the models are probably trained
more and more for that (e.g. Claude Artifacts), and now it has become for many
the default choice for sharing reports.&lt;/p&gt;
&lt;p&gt;How much of what we build comes from eliciting the same latent capabilities from
the same models?  Did the models make us prompt them that way?  Was it because
we shared ideas on Twitter and other communities that inspired us?  Or is it all
unrelated?&lt;/p&gt;
&lt;p&gt;There is something powerful and strange about how LLMs diffuse knowledge and
capabilities, while perhaps also nudging us all simultaniously and independently
toward building the same things.&lt;/p&gt;</description>
	<pubDate>Sat, 05 Sep 2026 00:00:00 +0000</pubDate>
</item>
<item>
	<title>Python Morsels: Creating temporary files in Python</title>
	<guid>https://www.pythonmorsels.com/temporary-files/</guid>
	<link>https://www.pythonmorsels.com/temporary-files/</link>
	<description>&lt;p&gt;How to create temporary files and directories in Python using the &lt;code&gt;tempfile&lt;/code&gt; module's &lt;code&gt;NamedTemporaryFile&lt;/code&gt; and &lt;code&gt;TemporaryDirectory&lt;/code&gt;.&lt;/p&gt;


&lt;div&gt;
  
    &lt;a href=&quot;https://www.pythonmorsels.com/temporary-files/&quot;&gt;&lt;img width=&quot;480&quot; height=&quot;270&quot; src=&quot;https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F2196808272-1faa9c1ea9afcbdca269693a805ce48bc95f4cbecfd1a1a5856668254582c2d2-d_1920x1080%3F%26region%3Dus&amp;src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png&quot; /&gt;&lt;/a&gt;
  
  &lt;p&gt;
  &lt;strong&gt;Table of contents&lt;/strong&gt;
  &lt;ol&gt;
  
  &lt;li&gt;&lt;a href=&quot;https://www.pythonmorsels.com/temporary-files/#making-a-temporary-file&quot; target=&quot;_blank&quot;&gt;Making a temporary file&lt;/a&gt;&lt;/li&gt;
  
  &lt;li&gt;&lt;a href=&quot;https://www.pythonmorsels.com/temporary-files/#leaving-temporary-files-open&quot; target=&quot;_blank&quot;&gt;Leaving temporary files open&lt;/a&gt;&lt;/li&gt;
  
  &lt;li&gt;&lt;a href=&quot;https://www.pythonmorsels.com/temporary-files/#making-a-temporary-directory&quot; target=&quot;_blank&quot;&gt;Making a temporary directory&lt;/a&gt;&lt;/li&gt;
  
  &lt;li&gt;&lt;a href=&quot;https://www.pythonmorsels.com/temporary-files/#you-probably-want-a-named-file&quot; target=&quot;_blank&quot;&gt;You probably want a named file&lt;/a&gt;&lt;/li&gt;
  
  &lt;li&gt;&lt;a href=&quot;https://www.pythonmorsels.com/temporary-files/#create-temporary-files-with-tempfile&quot; target=&quot;_blank&quot;&gt;Create temporary files with &lt;code&gt;tempfile&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
  
  &lt;/ol&gt;
  &lt;/p&gt;
&lt;/div&gt;
&lt;div&gt;
  
    &lt;h2&gt;Making a temporary file&lt;/h2&gt;
    
      
        &lt;p&gt;To make a temporary file in Python, you can use the &lt;code&gt;NamedTemporaryFile&lt;/code&gt; &lt;a href=&quot;https://www.pythonmorsels.com/what-is-a-context-manager/&quot; target=&quot;_blank&quot;&gt;context manager&lt;/a&gt; from the &lt;code&gt;tempfile&lt;/code&gt; module in Python's standard library:&lt;/p&gt;

      
        &lt;div class=&quot;codehilite&quot;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nn&quot;&gt;tempfile&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;NamedTemporaryFile&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;NamedTemporaryFile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mode&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;wt&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Temporary text.&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;The filename is &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;


      
        &lt;p&gt;Note this context manager will delete the file as soon as it exits, which may be a problem if we actually want to use the file after the context manager has exited:&lt;/p&gt;

      
        &lt;div class=&quot;codehilite&quot;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class=&quot;gp&quot;&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nn&quot;&gt;tempfile&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;NamedTemporaryFile&lt;/span&gt;
&lt;span class=&quot;gp&quot;&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;NamedTemporaryFile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mode&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;wt&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;gp&quot;&gt;... &lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Temporary text.&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;gp&quot;&gt;... &lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;filename&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;
&lt;span class=&quot;gp&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt;16&lt;/span&gt;
&lt;span class=&quot;gp&quot;&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;filename&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;gt&quot;&gt;Traceback (most recent call last):&lt;/span&gt;
  File &lt;span class=&quot;nb&quot;&gt;&quot;&amp;lt;stdin&amp;gt;&quot;&lt;/span&gt;, line &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;, in &lt;span class=&quot;n&quot;&gt;&amp;lt;module&amp;gt;&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;filename&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;pm&quot;&gt;~~~~^^^^^^^^^^&lt;/span&gt;
&lt;span class=&quot;gr&quot;&gt;FileNotFoundError&lt;/span&gt;: &lt;span class=&quot;n&quot;&gt;[Errno 2] No such file or directory: '/tmp/tmph52sw7ra'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;


      
        &lt;p&gt;Pretty much every time I make a temporary file, I need to close the file without actually deleting it so that I can then pass the filename off to other code that actually uses the file.&lt;/p&gt;

      
    
  
    &lt;h2&gt;Leaving temporary files open&lt;/h2&gt;
    
      &lt;p&gt;If you&amp;#x27;d like to make …&lt;/p&gt;
    
  
&lt;/div&gt;
&lt;h3&gt;&lt;a href=&quot;https://www.pythonmorsels.com/temporary-files/&quot; target=&quot;_blank&quot;&gt;Read the full article: https://www.pythonmorsels.com/temporary-files/&lt;/a&gt;&lt;/h3&gt;</description>
	<pubDate>Fri, 04 Sep 2026 14:30:00 +0000</pubDate>
</item>
<item>
	<title>Python Anywhere: Annual plans, PostgreSQL 15, and easier database management</title>
	<guid>https://blog.pythonanywhere.com/224/</guid>
	<link>https://blog.pythonanywhere.com/224/</link>
	<description>&lt;p&gt;We deployed our latest system update to our EU-based system on &lt;strong&gt;23 June 2026&lt;/strong&gt;
and to our US-based system on &lt;strong&gt;28 July 2026&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;People creating an account or upgrading from a free account can now choose an
annual plan. PostgreSQL 15 is available, and the Databases page now makes it
easier to restart PostgreSQL servers and keep track of MySQL storage limits.&lt;/p&gt;</description>
	<pubDate>Fri, 04 Sep 2026 10:00:00 +0000</pubDate>
</item>
<item>
	<title>Talk Python to Me: #561: TonIO, a Multi-threaded Async Runtime for Python</title>
	<guid>https://talkpython.fm/episodes/show/561/tonio-a-multi-threaded-async-runtime-for-python</guid>
	<link>https://talkpython.fm/episodes/show/561/tonio-a-multi-threaded-async-runtime-for-python</link>
	<description>How many cores does your machine have, 10, 18? Your async Python code uses just one of them. That isn't a bug in asyncio. That's the design, and optimizing event loops to be faster by 20% doesn't change it. So Giovanni Barillari started over. Joe is the creator of Granian, the Rust-based server that powers Talk Python. His new project is TonIO, an async runtime written from scratch for free-threaded Python. Real threads, a handful of primitives instead of asyncio's pile of them, and it flat out refuses to start if the GIL is on.&amp;lt;br/&amp;gt;
&amp;lt;br/&amp;gt;
&amp;lt;strong&amp;gt;Episode sponsors&amp;lt;/strong&amp;gt;&amp;lt;br/&amp;gt;
&amp;lt;br/&amp;gt;
&amp;lt;a href='https://talkpython.fm/sentry'&amp;gt;Sentry Error Monitoring, Code talkpython26&amp;lt;/a&amp;gt;&amp;lt;br&amp;gt;
&amp;lt;a href='https://talkpython.fm/devopsbook'&amp;gt;Python in Production&amp;lt;/a&amp;gt;&amp;lt;br&amp;gt;
&amp;lt;a href='https://talkpython.fm/training'&amp;gt;Talk Python Courses&amp;lt;/a&amp;gt;&amp;lt;br/&amp;gt;
&amp;lt;br/&amp;gt;
&amp;lt;h2 class=&quot;links-heading mb-4&quot;&amp;gt;Links from the show&amp;lt;/h2&amp;gt;
&amp;lt;div&amp;gt;&amp;lt;strong&amp;gt;Guest&amp;lt;/strong&amp;gt;&amp;lt;br/&amp;gt;
&amp;lt;strong&amp;gt;Giovanni Barillari&amp;lt;/strong&amp;gt;: &amp;lt;a href=&quot;https://github.com/gi0baro?featured_on=talkpython&quot; target=&quot;_blank&quot; &amp;gt;github.com&amp;lt;/a&amp;gt;&amp;lt;br/&amp;gt;
&amp;lt;br/&amp;gt;
&amp;lt;strong&amp;gt;Granian&amp;lt;/strong&amp;gt;: &amp;lt;a href=&quot;https://github.com/emmett-framework/granian?featured_on=talkpython&quot; target=&quot;_blank&quot; &amp;gt;github.com&amp;lt;/a&amp;gt;&amp;lt;br/&amp;gt;
&amp;lt;strong&amp;gt;Hyper&amp;lt;/strong&amp;gt;: &amp;lt;a href=&quot;https://github.com/hyperium/hyper?featured_on=talkpython&quot; target=&quot;_blank&quot; &amp;gt;github.com&amp;lt;/a&amp;gt;&amp;lt;br/&amp;gt;
&amp;lt;strong&amp;gt;Free threaded Python&amp;lt;/strong&amp;gt;: &amp;lt;a href=&quot;https://docs.python.org/3/howto/free-threading-python.html#freethreading-python-howto&quot; target=&quot;_blank&quot; &amp;gt;docs.python.org&amp;lt;/a&amp;gt;&amp;lt;br/&amp;gt;
&amp;lt;strong&amp;gt;Sort of&amp;lt;/strong&amp;gt;: &amp;lt;a href=&quot;https://labs.quansight.org/blog/free-threaded-one-year-recap?featured_on=talkpython&quot; target=&quot;_blank&quot; &amp;gt;labs.quansight.org&amp;lt;/a&amp;gt;&amp;lt;br/&amp;gt;
&amp;lt;strong&amp;gt;did a whole course&amp;lt;/strong&amp;gt;: &amp;lt;a href=&quot;https://training.talkpython.fm/courses/python-concurrency-deep-dive&quot; target=&quot;_blank&quot; &amp;gt;training.talkpython.fm&amp;lt;/a&amp;gt;&amp;lt;br/&amp;gt;
&amp;lt;strong&amp;gt;uvloop&amp;lt;/strong&amp;gt;: &amp;lt;a href=&quot;https://github.com/magicstack/uvloop?featured_on=talkpython&quot; target=&quot;_blank&quot; &amp;gt;github.com&amp;lt;/a&amp;gt;&amp;lt;br/&amp;gt;
&amp;lt;strong&amp;gt;rloop&amp;lt;/strong&amp;gt;: &amp;lt;a href=&quot;https://github.com/gi0baro/rloop?featured_on=talkpython&quot; target=&quot;_blank&quot; &amp;gt;github.com&amp;lt;/a&amp;gt;&amp;lt;br/&amp;gt;
&amp;lt;strong&amp;gt;TonIO&amp;lt;/strong&amp;gt;: &amp;lt;a href=&quot;https://github.com/gi0baro/tonio?featured_on=talkpython&quot; target=&quot;_blank&quot; &amp;gt;github.com&amp;lt;/a&amp;gt;&amp;lt;br/&amp;gt;
&amp;lt;strong&amp;gt;your EuroPython 2026 talk&amp;lt;/strong&amp;gt;: &amp;lt;a href=&quot;https://www.youtube.com/watch?v=3GwyadhJZBQ&amp;amp;amp;list=PLQHk3YPV3Dgk&amp;amp;amp;index=4&amp;amp;amp;t=753s&quot; target=&quot;_blank&quot; &amp;gt;www.youtube.com&amp;lt;/a&amp;gt;&amp;lt;br/&amp;gt;
&amp;lt;strong&amp;gt;Michael's Cutting Python Web App Memory Over 31% Article&amp;lt;/strong&amp;gt;: &amp;lt;a href=&quot;https://mkennedy.codes/posts/cutting-python-web-app-memory-over-31-percent/?featured_on=talkpython&quot; target=&quot;_blank&quot; &amp;gt;mkennedy.codes&amp;lt;/a&amp;gt;&amp;lt;br/&amp;gt;
&amp;lt;br/&amp;gt;
&amp;lt;strong&amp;gt;Watch this episode on YouTube&amp;lt;/strong&amp;gt;: &amp;lt;a href=&quot;https://www.youtube.com/watch?v=lmG0ocDTsZo&quot; target=&quot;_blank&quot; &amp;gt;youtube.com&amp;lt;/a&amp;gt;&amp;lt;br/&amp;gt;
&amp;lt;strong&amp;gt;Episode #561 deep-dive&amp;lt;/strong&amp;gt;: &amp;lt;a href=&quot;https://talkpython.fm/episodes/show/561/tonio-a-multi-threaded-async-runtime-for-python#takeaways-anchor&quot; target=&quot;_blank&quot; &amp;gt;talkpython.fm/561&amp;lt;/a&amp;gt;&amp;lt;br/&amp;gt;
&amp;lt;strong&amp;gt;Episode transcripts&amp;lt;/strong&amp;gt;: &amp;lt;a href=&quot;https://talkpython.fm/episodes/transcript/561/tonio-a-multi-threaded-async-runtime-for-python&quot; target=&quot;_blank&quot; &amp;gt;talkpython.fm&amp;lt;/a&amp;gt;&amp;lt;br/&amp;gt;
&amp;lt;br/&amp;gt;
&amp;lt;strong&amp;gt;Theme Song: Developer Rap&amp;lt;/strong&amp;gt;&amp;lt;br/&amp;gt;
&amp;lt;strong&amp;gt;🥁 Served in a Flask 🎸&amp;lt;/strong&amp;gt;: &amp;lt;a href=&quot;https://talkpython.fm/flasksong&quot; target=&quot;_blank&quot; &amp;gt;talkpython.fm/flasksong&amp;lt;/a&amp;gt;&amp;lt;br/&amp;gt;
&amp;lt;br/&amp;gt;
&amp;lt;strong&amp;gt;---==  Don't be a stranger  ==---&amp;lt;/strong&amp;gt;&amp;lt;br/&amp;gt;
&amp;lt;strong&amp;gt;YouTube&amp;lt;/strong&amp;gt;: &amp;lt;a href=&quot;https://talkpython.fm/youtube&quot; target=&quot;_blank&quot; &amp;gt;&amp;lt;i class=&quot;fa-brands fa-youtube&quot;&amp;gt;&amp;lt;/i&amp;gt; youtube.com/@talkpython&amp;lt;/a&amp;gt;&amp;lt;br/&amp;gt;
&amp;lt;br/&amp;gt;
&amp;lt;strong&amp;gt;Bluesky&amp;lt;/strong&amp;gt;: &amp;lt;a href=&quot;https://bsky.app/profile/talkpython.fm&quot; target=&quot;_blank&quot; &amp;gt;@talkpython.fm&amp;lt;/a&amp;gt;&amp;lt;br/&amp;gt;
&amp;lt;strong&amp;gt;Mastodon&amp;lt;/strong&amp;gt;: &amp;lt;a href=&quot;https://fosstodon.org/web/@talkpython&quot; target=&quot;_blank&quot; &amp;gt;&amp;lt;i class=&quot;fa-brands fa-mastodon&quot;&amp;gt;&amp;lt;/i&amp;gt; @talkpython@fosstodon.org&amp;lt;/a&amp;gt;&amp;lt;br/&amp;gt;
&amp;lt;strong&amp;gt;X.com&amp;lt;/strong&amp;gt;: &amp;lt;a href=&quot;https://x.com/talkpython&quot; target=&quot;_blank&quot; &amp;gt;&amp;lt;i class=&quot;fa-brands fa-twitter&quot;&amp;gt;&amp;lt;/i&amp;gt; @talkpython&amp;lt;/a&amp;gt;&amp;lt;br/&amp;gt;
&amp;lt;br/&amp;gt;
&amp;lt;strong&amp;gt;Michael on Bluesky&amp;lt;/strong&amp;gt;: &amp;lt;a href=&quot;https://bsky.app/profile/mkennedy.codes?featured_on=talkpython&quot; target=&quot;_blank&quot; &amp;gt;@mkennedy.codes&amp;lt;/a&amp;gt;&amp;lt;br/&amp;gt;
&amp;lt;strong&amp;gt;Michael on Mastodon&amp;lt;/strong&amp;gt;: &amp;lt;a href=&quot;https://fosstodon.org/web/@mkennedy&quot; target=&quot;_blank&quot; &amp;gt;&amp;lt;i class=&quot;fa-brands fa-mastodon&quot;&amp;gt;&amp;lt;/i&amp;gt; @mkennedy@fosstodon.org&amp;lt;/a&amp;gt;&amp;lt;br/&amp;gt;
&amp;lt;strong&amp;gt;Michael on X.com&amp;lt;/strong&amp;gt;: &amp;lt;a href=&quot;https://x.com/mkennedy?featured_on=talkpython&quot; target=&quot;_blank&quot; &amp;gt;&amp;lt;i class=&quot;fa-brands fa-twitter&quot;&amp;gt;&amp;lt;/i&amp;gt; @mkennedy&amp;lt;/a&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;/div&amp;gt;</description>
	<pubDate>Fri, 04 Sep 2026 07:33:34 +0000</pubDate>
</item>
<item>
	<title>Trey Hunner: Python Morsels now has spaced repetition</title>
	<guid>https://treyhunner.com/2026/09/python-morsels-now-has-spaced-repetition/</guid>
	<link>https://treyhunner.com/2026/09/python-morsels-now-has-spaced-repetition/</link>
	<description>&lt;p&gt;Nearly every book I&amp;rsquo;ve read on teaching and education over the past decade has talked about the value of &lt;strong&gt;spaced repetition&lt;/strong&gt;.
For much of that time, spaced repetition has been something I would &lt;em&gt;recommend&lt;/em&gt; learners do, but it wasn&amp;rsquo;t something I &lt;em&gt;helped&lt;/em&gt; anyone do&amp;hellip; until now.&lt;/p&gt;
&lt;p&gt;Python Morsels now has &lt;a href=&quot;https://www.pythonmorsels.com/recall/&quot;&gt;Daily Recall&lt;/a&gt;: a spaced repetition system &lt;strong&gt;for Python programmers&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;We learn by recalling, not by reading&lt;/h2&gt;
&lt;p&gt;The most effective learning techniques all rely on &lt;strong&gt;active recall&lt;/strong&gt;: trying to remember something &lt;em&gt;without&lt;/em&gt; looking it up, whether with flash cards, by explaining an idea in your own words, or by doing a task that requires it.&lt;/p&gt;
&lt;p&gt;We don&amp;rsquo;t learn by putting information &lt;em&gt;into&lt;/em&gt; our heads.
We learn by retrieving information &lt;em&gt;from&lt;/em&gt; our heads.
That&amp;rsquo;s why Python Morsels has always been built around exercises rather than videos: writing code is the most useful form of recall for a Python programmer.&lt;/p&gt;
&lt;p&gt;But not everything worth remembering warrants an entire Python exercise.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Which &lt;code&gt;uv&lt;/code&gt; command runs a tool without installing it?&lt;/li&gt;
&lt;li&gt;What is the time complexity of various list operations?&lt;/li&gt;
&lt;li&gt;Which string method can remove a substring from the end of a string?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A 20-minute exercise is overkill for practicing something that small.
But practicing it just once isn&amp;rsquo;t enough either.
A quick question, asked again just before you&amp;rsquo;d forget, is a much better fit.&lt;/p&gt;
&lt;h2&gt;Spaced repetition beats the forgetting curve&lt;/h2&gt;
&lt;p&gt;Many of the things I learned in school are long gone, especially the ones I haven&amp;rsquo;t thought about even once in &lt;em&gt;years&lt;/em&gt;.
I think I could explain photosynthesis in 9th grade.
I can&amp;rsquo;t today.&lt;/p&gt;
&lt;p&gt;This is explained by &lt;a href=&quot;https://en.wikipedia.org/wiki/Forgetting_curve&quot;&gt;the forgetting curve&lt;/a&gt;: we forget what we don&amp;rsquo;t recall, and the rate of forgetting is somewhat predictable.
&lt;strong&gt;Spaced repetition&lt;/strong&gt; is about using active recall to beat the forgetting curve.
Instead of recalling an idea over and over right after learning it, you wait until it has &lt;em&gt;started&lt;/em&gt; to fade, and then try to recall it.
Each successful recall earns a longer wait before the next one: minutes at first, then hours, then days, and eventually weeks and months.&lt;/p&gt;
&lt;p&gt;The tricky part is the timing: &lt;em&gt;when&lt;/em&gt; should you try to recall each thing?
That&amp;rsquo;s where an algorithm helps.
A spaced repetition system tracks every idea you&amp;rsquo;re trying to remember and prompts you to recall each one right before you&amp;rsquo;d forget it.&lt;/p&gt;
&lt;h2&gt;What Daily Recall does&lt;/h2&gt;
&lt;p&gt;Back in April, just before Earth Day, I made &lt;a href=&quot;https://whereabouts.earth&quot;&gt;Whereabouts.Earth&lt;/a&gt; to help me learn the name and location of every country in the world.
When I started, I could name about 90 of the 197 countries on a map.
By mid-June, with about 10 minutes of practice a day, I knew all of them.&lt;/p&gt;
&lt;p&gt;Daily Recall is the same idea, but for Python.
You pick the packs you want to practice, and each day it asks you a few questions from them.
Answer a question correctly and it&amp;rsquo;ll be a while before you see that one again.
Miss it and you&amp;rsquo;ll see it again soon.&lt;/p&gt;
&lt;p&gt;Daily Recall uses &lt;a href=&quot;https://github.com/open-spaced-repetition/fsrs4anki/wiki/ABC-of-FSRS&quot;&gt;FSRS&lt;/a&gt; for scheduling, which is one of the most effective spaced repetition algorithms (it&amp;rsquo;s an option in the &lt;a href=&quot;https://apps.ankiweb.net/&quot;&gt;Anki&lt;/a&gt; flash card app).&lt;/p&gt;
&lt;p&gt;If you have trouble remembering which operations on different data structures are fast and which are slow, there&amp;rsquo;s a &lt;a href=&quot;https://www.pythonmorsels.com/recall/packs/time-complexity/&quot;&gt;Time Complexity pack&lt;/a&gt; for that.
If you&amp;rsquo;re struggling to remember the &lt;em&gt;many&lt;/em&gt; different subcommands that &lt;code&gt;uv&lt;/code&gt; supports, there&amp;rsquo;s a &lt;a href=&quot;https://www.pythonmorsels.com/recall/packs/uv/&quot;&gt;uv pack&lt;/a&gt; for that.
There are also &lt;a href=&quot;https://www.pythonmorsels.com/recall/packs/&quot;&gt;packs&lt;/a&gt; on string methods, built-in functions, dictionaries, iterable unpacking, f-strings, pytest, and what&amp;rsquo;s new in Python 3.13 and 3.14.
I&amp;rsquo;m hoping to release about one new pack each week over the next many months.&lt;/p&gt;
&lt;p&gt;Daily Recall also &lt;strong&gt;works well on a phone&lt;/strong&gt; because recall questions don&amp;rsquo;t require typing a bunch of code.
So you can &lt;strong&gt;replace 5 minutes of your daily doomscrolling&lt;/strong&gt; with 5 minutes of extra Python learning.&lt;/p&gt;
&lt;p&gt;Early users who practiced about 5 minutes a day ended their first month with 25 to 50 new things they could still recall weeks after last seeing them.&lt;/p&gt;
&lt;h2&gt;You can use Daily Recall for free&lt;/h2&gt;
&lt;p&gt;Most &lt;a href=&quot;https://www.pythonmorsels.com/recall/&quot;&gt;Daily Recall&lt;/a&gt; packs are free, and the rest are included with the &lt;a href=&quot;https://www.pythonmorsels.com/pricing/&quot;&gt;All Access&lt;/a&gt; plan.
To get started, &lt;a href=&quot;https://www.pythonmorsels.com/accounts/signup/&quot;&gt;create a free Python Morsels account&lt;/a&gt;, pick a pack or two, and answer a few questions.&lt;/p&gt;
&lt;p&gt;And whether or not you ever use Daily Recall, I&amp;rsquo;d recommend spaced repetition.
If you&amp;rsquo;d rather write your own flash cards, on Python or anything else, &lt;a href=&quot;https://apps.ankiweb.net/&quot;&gt;Anki&lt;/a&gt; works well too.
The next time you learn something new in Python, don&amp;rsquo;t just read it a second time.
Try to recall it tomorrow, and then again next week.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.pythonmorsels.com/recall/&quot; class=&quot;subscribe-btn form-big&quot;&gt;Try Daily Recall&lt;/a&gt;&lt;/p&gt;</description>
	<pubDate>Fri, 04 Sep 2026 00:30:00 +0000</pubDate>
</item>
<item>
	<title>Bob Belderbos: Why does Alembic need an import you never use?</title>
	<guid>https://belderbos.dev/blog/what-table-true-does-sqlmodel/</guid>
	<link>https://belderbos.dev/blog/what-table-true-does-sqlmodel/</link>
	<description>&lt;p&gt;A coaching session this week surfaced a good question: Alembic autogenerate only works if you import your models first, even though you never reference them.&lt;/p&gt;
&lt;p&gt;Ruff flags the import as unused.&lt;/p&gt;
&lt;p&gt;So why is it there?&lt;/p&gt;
&lt;p&gt;Instead of just adding &lt;code&gt;# noqa: F401&lt;/code&gt;, I stopped and followed the call path into SQLModel and SQLAlchemy. The answer is a nice example of Python &lt;em&gt;metaprogramming&lt;/em&gt;: importing the module executes the class definition, and the class definition registers its table with SQLAlchemy's metadata.&lt;/p&gt;
&lt;span id=&quot;continue-reading&quot;&gt;&lt;/span&gt;&lt;h2 id=&quot;the-unused-import-that-isn-t-unused&quot;&gt;The unused import that isn't unused&lt;/h2&gt;
&lt;p&gt;Here's the line every SQLModel + Alembic setup has in &lt;code&gt;env.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;from&lt;/span&gt;&lt;span&gt; tips.models&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; import&lt;/span&gt;&lt;span&gt; Tip, User&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-comment&quot;&gt;  # noqa: F401&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;target_metadata&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; =&lt;/span&gt;&lt;span&gt; SQLModel.metadata&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(That's a real line from &lt;a rel=&quot;noopener external&quot; target=&quot;_blank&quot; href=&quot;https://github.com/bbelderbos/codeimag.es/blob/main/migrations/env.py#L22&quot;&gt;this project's &lt;code&gt;env.py&lt;/code&gt;&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SQLModel.metadata&lt;/code&gt; is a single shared &lt;code&gt;MetaData&lt;/code&gt; object. A table only gets registered in its tables collection as a side effect of the class being defined.&lt;/p&gt;
&lt;p&gt;If &lt;code&gt;tips.models&lt;/code&gt; never gets imported, the &lt;code&gt;class Tip(...)&lt;/code&gt; statement never executes, so no table is registered. Alembic then sees empty model metadata when it runs autogenerate. Existing database tables aren't present in that metadata, so Alembic can propose dropping them.&lt;/p&gt;
&lt;p&gt;So the import isn't there for the names &lt;code&gt;Tip&lt;/code&gt; and &lt;code&gt;User&lt;/code&gt;. It's there to &lt;em&gt;run the module&lt;/em&gt; so the class definitions fire. That's why &lt;code&gt;# noqa: F401&lt;/code&gt; is justified.&lt;/p&gt;
&lt;p&gt;This is a common confusion for people new to Python, importing an object actually reads and executes the module, and the side effects of that execution are often more important than the names it exports. (For a related trap, where &lt;em&gt;when&lt;/em&gt; an object gets created changes its behavior, see &lt;a href=&quot;https://belderbos.dev/blog/two-scoping-bugs-object-lifetimes/&quot;&gt;Two Python Scoping Bugs&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;A better way to write it so we can include any future models without having to list them all is:&lt;/p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;import&lt;/span&gt;&lt;span&gt; tips.models&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-comment&quot;&gt;  # noqa: F401&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The interesting question is what &quot;the class definition fires&quot; actually means. Nothing in your model class calls &lt;code&gt;metadata.add_table()&lt;/code&gt;. So who does?&lt;/p&gt;
&lt;h2 id=&quot;following-table-true-into-the-source&quot;&gt;Following table=True into the source&lt;/h2&gt;
&lt;p&gt;When you write &lt;code&gt;class Tip(SQLModel, table=True)&lt;/code&gt;, the &lt;code&gt;table=True&lt;/code&gt; is a class keyword argument. It gets routed to &lt;code&gt;SQLModelMetaclass&lt;/code&gt;. In sqlmodel 0.0.39, &lt;code&gt;main.py&lt;/code&gt; reads it back out and stashes it on the model config:&lt;/p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-comment&quot;&gt;# sqlmodel/main.py — SQLModelMetaclass.__new__&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;config_table&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; =&lt;/span&gt;&lt;span&gt; get_config(&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;&amp;quot;table&amp;quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;if&lt;/span&gt;&lt;span&gt; config_table&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; is&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt; True&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    new_cls.model_config[&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;&amp;quot;table&amp;quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; =&lt;/span&gt;&lt;span&gt; config_table&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-constant&quot;&gt;    ...&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then &lt;code&gt;__init__&lt;/code&gt; on the same metaclass checks that flag and, only when it's set, hands the class over to SQLAlchemy's declarative machinery:&lt;/p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-comment&quot;&gt;# sqlmodel/main.py — SQLModelMetaclass.__init__&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;base_is_table&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-support&quot;&gt; any&lt;/span&gt;&lt;span&gt;(is_table_model_class(base)&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; for&lt;/span&gt;&lt;span&gt; base&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; in&lt;/span&gt;&lt;span&gt; bases)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;if&lt;/span&gt;&lt;span&gt; is_table_model_class(&lt;/span&gt;&lt;span class=&quot;z-variable z-language&quot;&gt;cls&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; and not&lt;/span&gt;&lt;span&gt; base_is_table:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-constant&quot;&gt;    ...&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-comment&quot;&gt;  # build columns and relationships from the model's fields&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    DeclarativeMeta.&lt;/span&gt;&lt;span class=&quot;z-support&quot;&gt;__init__&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-variable z-language&quot;&gt;cls&lt;/span&gt;&lt;span&gt;, classname, bases, dict_,&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; **&lt;/span&gt;&lt;span&gt;kw)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;else&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ModelMetaclass.&lt;/span&gt;&lt;span class=&quot;z-support&quot;&gt;__init__&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-variable z-language&quot;&gt;cls&lt;/span&gt;&lt;span&gt;, classname, bases, dict_,&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; **&lt;/span&gt;&lt;span&gt;kw)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(&lt;code&gt;is_table_model_class&lt;/code&gt; is just the check that &lt;code&gt;model_config[&quot;table&quot;]&lt;/code&gt; is set.)&lt;/p&gt;
&lt;p&gt;That &lt;code&gt;if/else&lt;/code&gt; is the key fork created by &lt;code&gt;table=True&lt;/code&gt;. With it, SQLModel takes the class down SQLAlchemy's declarative path. Without it, you get a SQLModel/Pydantic model rather than a mapped table model, so no SQLAlchemy &lt;code&gt;Table&lt;/code&gt; is registered in &lt;code&gt;SQLModel.metadata&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;From &lt;code&gt;DeclarativeMeta.__init__&lt;/code&gt; the trail runs straight down into SQLAlchemy:&lt;/p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-comment&quot;&gt;# sqlalchemy/orm/decl_api.py&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;if not&lt;/span&gt;&lt;span class=&quot;z-variable z-language&quot;&gt; cls&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span class=&quot;z-support z-variable&quot;&gt;__dict__&lt;/span&gt;&lt;span&gt;.get(&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;&amp;quot;__abstract__&amp;quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt; False&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    _as_declarative(reg,&lt;/span&gt;&lt;span class=&quot;z-variable z-language&quot;&gt; cls&lt;/span&gt;&lt;span&gt;, dict_)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;_as_declarative&lt;/code&gt; scans the class, builds a &lt;code&gt;Table&lt;/code&gt; object from the columns, and that &lt;code&gt;Table&lt;/code&gt; registers itself:&lt;/p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-comment&quot;&gt;# sqlalchemy/sql/schema.py — Table.__new__&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;metadata._add_table(name, schema, table)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-comment&quot;&gt;# sqlalchemy/sql/schema.py — MetaData._add_table&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-storage z-type&quot;&gt;def&lt;/span&gt;&lt;span class=&quot;z-entity z-name&quot;&gt; _add_table&lt;/span&gt;&lt;span class=&quot;z-variable z-parameter z-function&quot;&gt;(self, name, schema, table):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    key&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; =&lt;/span&gt;&lt;span&gt; _get_table_key(name, schema)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-variable z-language&quot;&gt;    self&lt;/span&gt;&lt;span&gt;.tables._insert_item(key, table)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There it is. &lt;code&gt;self.tables._insert_item(...)&lt;/code&gt; is the exact moment your model becomes an entry in &lt;code&gt;SQLModel.metadata.tables&lt;/code&gt;, and it runs during class definition, triggered by importing the module. That's the side effect Alembic depends on.&lt;/p&gt;
&lt;h2 id=&quot;seeing-it-happen&quot;&gt;Seeing it happen&lt;/h2&gt;
&lt;p&gt;You don't have to trust the call path. Registration is a side effect of the &lt;code&gt;class&lt;/code&gt; statement, so a plain REPL lets you watch the registry grow in real time:&lt;/p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;&amp;gt;&amp;gt;&amp;gt; from&lt;/span&gt;&lt;span&gt; sqlmodel&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; import&lt;/span&gt;&lt;span&gt; SQLModel, Field&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-support&quot;&gt; print&lt;/span&gt;&lt;span&gt;(SQLModel.metadata.tables)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;FacadeDict({})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword z-storage z-type&quot;&gt;&amp;gt;&amp;gt;&amp;gt; class&lt;/span&gt;&lt;span class=&quot;z-entity z-name&quot;&gt; Tip&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-entity&quot;&gt;SQLModel&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-variable&quot;&gt; table&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt;True&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-constant z-support&quot;&gt;...     id&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span class=&quot;z-support&quot;&gt; int&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; |&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt; None&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; =&lt;/span&gt;&lt;span&gt; Field(&lt;/span&gt;&lt;span class=&quot;z-variable&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt;None&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-variable&quot;&gt; primary_key&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt;True&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-constant&quot;&gt;...&lt;/span&gt;&lt;span&gt;     text:&lt;/span&gt;&lt;span class=&quot;z-support&quot;&gt; str&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-constant&quot;&gt;...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-support&quot;&gt; print&lt;/span&gt;&lt;span&gt;(SQLModel.metadata.tables)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;FacadeDict({&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;'tip'&lt;/span&gt;&lt;span&gt;: Table(&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;'tip'&lt;/span&gt;&lt;span&gt;, MetaData(), Column(&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;'id'&lt;/span&gt;&lt;span&gt;, Integer(),&lt;/span&gt;&lt;span class=&quot;z-variable&quot;&gt; table&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt;=&amp;lt;&lt;/span&gt;&lt;span&gt;tip&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-variable&quot;&gt; primary_key&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt;True&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-variable&quot;&gt; nullable&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt;False&lt;/span&gt;&lt;span&gt;), Column(&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;'text'&lt;/span&gt;&lt;span&gt;, AutoString(),&lt;/span&gt;&lt;span class=&quot;z-variable&quot;&gt; table&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt;=&amp;lt;&lt;/span&gt;&lt;span&gt;tip&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-variable&quot;&gt; nullable&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt;False&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;span class=&quot;z-variable&quot;&gt; schema&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt;None&lt;/span&gt;&lt;span&gt;)})&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No &lt;code&gt;create_engine&lt;/code&gt;, no &lt;code&gt;create_all&lt;/code&gt;, no import of your models module. Just defining the class populated the shared &lt;code&gt;metadata&lt;/code&gt;. Drop the &lt;code&gt;table=True&lt;/code&gt; and run it again: the dict stays empty, because now &lt;code&gt;Tip&lt;/code&gt; is a SQLModel/Pydantic model rather than a mapped table model.&lt;/p&gt;
&lt;p&gt;That proves the &lt;em&gt;class statement&lt;/em&gt; does the registering. To tie it back to the opening question, put the same class in a module and let the import fire it:&lt;/p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-comment&quot;&gt;# models.py&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;from&lt;/span&gt;&lt;span&gt; sqlmodel&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; import&lt;/span&gt;&lt;span&gt; SQLModel, Field&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-storage z-type&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;z-entity z-name&quot;&gt; Tip&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-entity&quot;&gt;SQLModel&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-variable&quot;&gt; table&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt;True&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-support&quot;&gt;    id&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span class=&quot;z-support&quot;&gt; int&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; |&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt; None&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; =&lt;/span&gt;&lt;span&gt; Field(&lt;/span&gt;&lt;span class=&quot;z-variable&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt;None&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-variable&quot;&gt; primary_key&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt;True&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    text:&lt;/span&gt;&lt;span class=&quot;z-support&quot;&gt; str&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;&amp;gt;&amp;gt;&amp;gt; from&lt;/span&gt;&lt;span&gt; sqlmodel&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; import&lt;/span&gt;&lt;span&gt; SQLModel&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-support&quot;&gt; print&lt;/span&gt;&lt;span&gt;(SQLModel.metadata.tables)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;FacadeDict({})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;&amp;gt;&amp;gt;&amp;gt; import&lt;/span&gt;&lt;span&gt; models&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-comment&quot;&gt;  # the &amp;quot;unused&amp;quot; import&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-support&quot;&gt; print&lt;/span&gt;&lt;span&gt;(SQLModel.metadata.tables)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;FacadeDict({&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;'tip'&lt;/span&gt;&lt;span&gt;: Table(&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;'tip'&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt; ...&lt;/span&gt;&lt;span&gt;)})&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You never touch &lt;code&gt;models.Tip&lt;/code&gt; after importing it, yet the registry filled up. That's the exact &lt;code&gt;env.py&lt;/code&gt; situation: the import runs the module, the module runs the class definition, and the class definition registers the table.&lt;/p&gt;
&lt;h2 id=&quot;is-this-metaprogramming&quot;&gt;Is this metaprogramming?&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;SQLModelMetaclass&lt;/code&gt; inherits from both Pydantic's &lt;code&gt;ModelMetaclass&lt;/code&gt; and SQLAlchemy's &lt;code&gt;DeclarativeMeta&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This is the same mechanism behind a lot of Python you use daily. If you've ever wondered how a class declaration can acquire behavior you never explicitly wrote, the answer is often a metaclass or &lt;code&gt;__init_subclass__&lt;/code&gt; doing work at definition time.&lt;/p&gt;
&lt;p&gt;A metaclass is code that runs when a class is defined. Here it inspects a keyword, processes the model's fields, and delegates table-model construction to SQLAlchemy.&lt;/p&gt;
&lt;p&gt;Your field annotations get read at that moment and turned into &lt;code&gt;Column&lt;/code&gt; objects.&lt;/p&gt;
&lt;p&gt;The practical payoff: next time autogenerate produces an empty migration or wants to drop all your tables, you'll know the cause is registration order, not Alembic being broken. Something imported the models too late, or not at all, and &lt;code&gt;metadata.tables&lt;/code&gt; was empty when Alembic read it.&lt;/p&gt;
&lt;h2 id=&quot;looking-at-libraries&quot;&gt;Looking at libraries&lt;/h2&gt;
&lt;p&gt;Reading a library's source to answer &quot;why is this import here&quot; is a great way to learn the library and Python itself. You don't have to understand every line. Follow the call path of a single feature and you'll often see how the pieces fit together.&lt;/p&gt;
&lt;p&gt;It's also about not taking things at face value. A &lt;code&gt;noqa&lt;/code&gt; means you're deliberately silencing something a linter flagged. Here it was warranted, but only after understanding &lt;em&gt;why&lt;/em&gt; the import exists.&lt;/p&gt;
&lt;p&gt;AI can follow this call path for you in seconds. That's useful.&lt;/p&gt;
&lt;p&gt;But the valuable skill isn't memorizing that &lt;code&gt;Table.__new__&lt;/code&gt; calls &lt;code&gt;_add_table&lt;/code&gt;. It's having the instinct to stop when something looks strange and ask: &lt;strong&gt;why is this here?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;As AI writes more of the code, this habit matters now more than ever I think. If you don't understand the code, you can't maintain / improve it over time.&lt;/p&gt;
&lt;h2 id=&quot;keep-reading&quot;&gt;Keep reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://belderbos.dev/blog/classmethod-vs-staticmethod-vs-instance-method-python/&quot;&gt;When to use classmethod, staticmethod, or instance method in Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://belderbos.dev/blog/event-sourcing-python-store-events-not-state/&quot;&gt;Event sourcing in Python: store events, not state&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
	<pubDate>Fri, 04 Sep 2026 00:00:00 +0000</pubDate>
</item>
<item>
	<title>Graham Dumpleton: Phased behaviour in wrapture</title>
	<guid>https://grahamdumpleton.me/posts/2026/09/phased-behaviour-in-wrapture/</guid>
	<link>https://grahamdumpleton.me/posts/2026/09/phased-behaviour-in-wrapture/</link>
	<description>&lt;p&gt;Most of what a test configures on a patch holds until the test changes it. Retry logic is the classic case where that is not enough: the code under test keeps calling, and the test needs the behaviour to change on its own as it does. Fail twice and then succeed. Hand out a sequence of canned responses. Run the real thing until it breaks and then fail fast. &lt;code&gt;unittest.mock&lt;/code&gt; handles the first two of those with a list passed as &lt;code&gt;side_effect&lt;/code&gt;, consumed one entry per call. wrapture models the same idea as phases, and this post is about what that buys you beyond the list.&lt;/p&gt;
&lt;h2&gt;The code under test&lt;/h2&gt;
&lt;p&gt;A client that fetches a URL, and a function that retries on a timeout:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class Client:
    def fetch(self, url):
        if &amp;quot;bad&amp;quot; in url:
            raise ConnectionError(f&amp;quot;cannot reach {url}&amp;quot;)
        return {&amp;quot;url&amp;quot;: url, &amp;quot;status&amp;quot;: 200}


def fetch_with_retry(client, url, attempts=3):
    for attempt in range(1, attempts + 1):
        try:
            return client.fetch(url)
        except TimeoutError:
            if attempt == attempts:
                raise
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With mock the retry test is a &lt;code&gt;side_effect&lt;/code&gt; list, and it works fine:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;with patch.object(Client, &amp;quot;fetch&amp;quot;, side_effect=[TimeoutError(&amp;quot;busy&amp;quot;), TimeoutError(&amp;quot;busy&amp;quot;), {&amp;quot;url&amp;quot;: &amp;quot;/x&amp;quot;, &amp;quot;status&amp;quot;: 200}]):
    assert fetch_with_retry(Client(), &amp;quot;/x&amp;quot;) == {&amp;quot;url&amp;quot;: &amp;quot;/x&amp;quot;, &amp;quot;status&amp;quot;: 200}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What the list cannot say is &amp;quot;and then run the real code&amp;quot;. Every entry is a fabricated outcome, so the third call is a canned dictionary rather than the real &lt;code&gt;fetch()&lt;/code&gt;, and the test proves the loop retries but not that the real method is what it eventually reaches.&lt;/p&gt;
&lt;h2&gt;Phases&lt;/h2&gt;
&lt;p&gt;In wrapture the behaviour configured on &lt;code&gt;on_call&lt;/code&gt; is phase 0, and &lt;code&gt;then()&lt;/code&gt; adds the phase that takes over from it, with the argument saying when the hand-over happens. Each phase is a complete behaviour of its own with the full vocabulary, and nothing is inherited between them, so a phase with no terminal runs the real operation:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;fetch = wrapture.binding(Client, &amp;quot;fetch&amp;quot;)
fetch.on_call.raises(TimeoutError(&amp;quot;busy&amp;quot;))

recovered = fetch.on_call.then(after=2)
recovered.passes_through()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first two calls raise, and every call after that is real. Stating &lt;code&gt;passes_through()&lt;/code&gt; on a fresh phase is optional, since that is what an empty phase does anyway, but worth writing when running the real thing is the point of the phase. Recording it shows the hand-over, and the tape marks which outcomes were injected and which were real:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;with wrapture.timeline(fetch) as tape:
    print(fetch_with_retry(Client(), &amp;quot;/orders&amp;quot;))
    print(tape.tree())
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;{'url': '/orders', 'status': 200}
__main__:Client.fetch(url='/orders')  !! TimeoutError (injected)
__main__:Client.fetch(url='/orders')  !! TimeoutError (injected)
__main__:Client.fetch(url='/orders')  -&amp;gt; {'url': '/orders', 'status': 200}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each event carries the index of the phase that handled it, so the recording can be filtered by regime, and the binding knows which phase it is in:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;fetch.events.in_phase(0).assert_times(2)
fetch.events.in_phase(1).assert_once()
assert fetch.phase == 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;binding.phase&lt;/code&gt; is the index of the phase currently active, and &lt;code&gt;in_phase()&lt;/code&gt; filters the recorded events to those a given phase handled. The two answer different questions, since a phase can be entered and left without handling a call. Phases restart at 0 on every &lt;code&gt;apply()&lt;/code&gt;, so a binding handed to &lt;code&gt;timeline()&lt;/code&gt; starts its script afresh in each test that uses it.&lt;/p&gt;
&lt;p&gt;The give-up path is the same binding with a bigger count. With &lt;code&gt;then(after=3)&lt;/code&gt; all three attempts raise, &lt;code&gt;fetch_with_retry()&lt;/code&gt; re-raises the last one, and the tape shows three injected failures and no real call.&lt;/p&gt;
&lt;p&gt;The verbs on a phase return the phase, so a phase can be configured in one chain, &lt;code&gt;then(after=1).validates_args(check).returns(b)&lt;/code&gt;. Holding it in a variable named for what the phase is, and configuring it line by line as with &lt;code&gt;on_call&lt;/code&gt;, usually reads better, and it is the style I would use in a test.&lt;/p&gt;
&lt;h2&gt;Ending a phase on a condition&lt;/h2&gt;
&lt;p&gt;A count is one of three ways a phase can end. &lt;code&gt;then(until=fn)&lt;/code&gt; ends the phase once &lt;code&gt;fn(event)&lt;/code&gt; is true for a call it handled. The event is the same one a timeline would record, seen as the caller saw it, so the condition can look at the arguments, the result, or whether the call raised. That is enough to build a circuit breaker: run the real call until one fails, then fail fast without touching the remote at all.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class CircuitOpen(Exception):
    pass


def failed(event):
    return event.exception is not None


fetch = wrapture.binding(Client, &amp;quot;fetch&amp;quot;)
fetch.on_call.passes_through()

tripped = fetch.on_call.then(until=failed)
tripped.raises(CircuitOpen(&amp;quot;circuit open&amp;quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fetch two good URLs, one bad one that the real &lt;code&gt;fetch()&lt;/code&gt; rejects, and then another good one:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;__main__:Client.fetch(url='/a')  -&amp;gt; {'url': '/a', 'status': 200}
__main__:Client.fetch(url='/b')  -&amp;gt; {'url': '/b', 'status': 200}
__main__:Client.fetch(url='/bad')  !! ConnectionError
__main__:Client.fetch(url='/c')  !! CircuitOpen (injected)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;ConnectionError&lt;/code&gt; is real, raised by the real method for a real reason, and the &lt;code&gt;CircuitOpen&lt;/code&gt; after it is the binding's. A &lt;code&gt;side_effect&lt;/code&gt; list has no way to express a phase whose boundary depends on what the real code did.&lt;/p&gt;
&lt;h2&gt;Sequences&lt;/h2&gt;
&lt;p&gt;For &amp;quot;return the next value on each call&amp;quot; a phase per value would be tiresome, so &lt;code&gt;returns_from(iterable)&lt;/code&gt; is a terminal that draws successive values, one per call, lazily. A generator or &lt;code&gt;itertools.cycle()&lt;/code&gt; works. When the sequence runs out the phase ends and the call that found it empty is handled by the successor, so a bare &lt;code&gt;then()&lt;/code&gt; after a sequence means &amp;quot;when it is exhausted&amp;quot;. A polling loop is the natural example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class Job:
    def status(self):
        return &amp;quot;done&amp;quot;


def wait_for(job, polls=5):
    for _ in range(polls):
        if job.status() == &amp;quot;done&amp;quot;:
            return True
    return False


status = wrapture.binding(Job, &amp;quot;status&amp;quot;)
status.on_call.returns_from([&amp;quot;queued&amp;quot;, &amp;quot;running&amp;quot;, &amp;quot;running&amp;quot;])

settled = status.on_call.then()
settled.returns(&amp;quot;done&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;__main__:Job.status()  -&amp;gt; 'queued' (injected)
__main__:Job.status()  -&amp;gt; 'running' (injected)
__main__:Job.status()  -&amp;gt; 'running' (injected)
__main__:Job.status()  -&amp;gt; 'done' (injected)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the closest thing to mock's &lt;code&gt;side_effect&lt;/code&gt; list, and the deliberate difference is that values and exceptions are kept apart. &lt;code&gt;side_effect=[a, b, Err]&lt;/code&gt; becomes &lt;code&gt;returns_from([a, b])&lt;/code&gt; followed by a phase that &lt;code&gt;raises(Err)&lt;/code&gt;, which is more lines for the same three outcomes but each phase says what it is. Running out with no successor is a loud &lt;code&gt;SequenceExhaustedError&lt;/code&gt; at the call site rather than a &lt;code&gt;StopIteration&lt;/code&gt; leaking out of the code under test, and the message says to add a phase with &lt;code&gt;then()&lt;/code&gt; or supply an endless sequence.&lt;/p&gt;
&lt;p&gt;A known sequence of &amp;quot;random&amp;quot; numbers is another use, making code that jitters or samples deterministic without seeding tricks: &lt;code&gt;binding(random, &amp;quot;random&amp;quot;).on_call.returns_from([0.1, 0.9, 0.5])&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Advancing from outside&lt;/h2&gt;
&lt;p&gt;The third way a phase ends is that something other than this binding's own calls decides it should. A bare &lt;code&gt;then()&lt;/code&gt; with no condition ends only when the test calls &lt;code&gt;binding.advance()&lt;/code&gt;, which also works whatever the exit condition, so a test can force the next phase early. The simplest use is a test that sits between calls:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;remote = wrapture.binding(Client, &amp;quot;fetch&amp;quot;)
remote.on_call.raises(ConnectionError(&amp;quot;down&amp;quot;))
remote.on_call.then().passes_through()

with remote:
    client = Client()

    with pytest.raises(ConnectionError):
        client.fetch(&amp;quot;/x&amp;quot;)

    remote.advance()
    assert client.fetch(&amp;quot;/x&amp;quot;)[&amp;quot;status&amp;quot;] == 200
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The more interesting use is when the trigger lives in a different binding. Here the remote stays down until a health check, itself a binding, reports it healthy, and the health check's own result stage advances the remote:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class Monitor:
    def check(self):
        return &amp;quot;healthy&amp;quot;


remote = wrapture.binding(Client, &amp;quot;fetch&amp;quot;)
remote.on_call.raises(ConnectionError(&amp;quot;down&amp;quot;))

online = remote.on_call.then()
online.passes_through()

health = wrapture.binding(Monitor, &amp;quot;check&amp;quot;)
health.on_call.returns_from([&amp;quot;unhealthy&amp;quot;, &amp;quot;unhealthy&amp;quot;, &amp;quot;healthy&amp;quot;])
health.on_call.then().returns(&amp;quot;healthy&amp;quot;)


def note_recovery(result):
    if result == &amp;quot;healthy&amp;quot;:
        remote.advance()


health.on_call.validates_result(note_recovery)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run code that polls the monitor and tries the client each time round, and the tape shows the two scripts interleaving:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;__main__:Monitor.check()  -&amp;gt; 'unhealthy' (injected)
__main__:Client.fetch(url='/x')  !! ConnectionError (injected)
__main__:Monitor.check()  -&amp;gt; 'unhealthy' (injected)
__main__:Client.fetch(url='/x')  !! ConnectionError (injected)
__main__:Monitor.check()  -&amp;gt; 'healthy' (injected)
__main__:Client.fetch(url='/x')  -&amp;gt; {'url': '/x', 'status': 200}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that a stage such as &lt;code&gt;validates_result()&lt;/code&gt; belongs to the phase it was configured on, which follows from phases inheriting nothing from each other. That is why &lt;code&gt;&amp;quot;healthy&amp;quot;&lt;/code&gt; is the last value of the phase 0 sequence above rather than the value the successor phase returns; if the stage were on phase 0 and the triggering value only ever came from phase 1, the recovery would never be noticed. A stage that should run in every phase is configured in every phase. When the condition is visible in the binding's own calls, &lt;code&gt;then(until=...)&lt;/code&gt; says it more directly than a stage calling &lt;code&gt;advance()&lt;/code&gt;, and is the form to reach for first.&lt;/p&gt;
&lt;h2&gt;Where phases fit in a test&lt;/h2&gt;
&lt;p&gt;Phases are for behaviour that must change within one call of the code under test, as it happens with a retry loop, a breaker, or a polling wait. A test that sits between calls does not need them; it reconfigures the binding in place, &lt;code&gt;on_call.returns(...)&lt;/code&gt; again, and carries on. That is why the decorator form deliberately leaves &lt;code&gt;then()&lt;/code&gt; out of its chain: how behaviour changes over time is the test's script, and it is configured in the body through the injected handle, where the phase markers can be given names.&lt;/p&gt;
&lt;p&gt;The attribute channels have phases too, &lt;code&gt;on_get&lt;/code&gt; in particular has &lt;code&gt;returns_from()&lt;/code&gt;, so a module constant can read one way for two reads and then another, which I will come back to in the next post. And &lt;code&gt;passes_through()&lt;/code&gt; on a base namespace clears phase 0 only; to drop the whole chain and start again, &lt;code&gt;on_call.reset()&lt;/code&gt; is the tool.&lt;/p&gt;
&lt;h2&gt;What's next&lt;/h2&gt;
&lt;p&gt;Everything in this series so far has been about calls. The next post is about everything a binding can name that is not a call: attribute reads and writes, a value held in a slot for the duration of a test, the whole content of a settings dict, and what happens item by item as a generator is consumed.&lt;/p&gt;</description>
	<pubDate>Fri, 04 Sep 2026 00:00:00 +0000</pubDate>
</item>
<item>
	<title>Jaime Buelta: The Many Challenges in Integrating Information for AI Agents</title>
	<guid>https://wrongsideofmemphis.com/2026/09/03/the-many-challenges-in-integrating-information-for-ai-agents/</guid>
	<link>https://wrongsideofmemphis.com/2026/09/03/the-many-challenges-in-integrating-information-for-ai-agents/</link>
	<description>Recently I&amp;#8217;ve been thinking quite a lot about information availability for agents, and the fact that this is a very difficult and potentially irresoluble problem. Let me try to explain myself. I talked before about a mental model on differentiating between the LLM models and the tools that access those models. I think that now that&amp;#8217;s clearer as we are using more and more agents. We understand that we can use Claude Code with different models (like Sonnet or Opus) that change the capacity of the agent, but not its capabilities. The... &lt;a class=&quot;read-more&quot; href=&quot;https://wrongsideofmemphis.com/2026/09/03/the-many-challenges-in-integrating-information-for-ai-agents/&quot;&gt;Read More&lt;/a&gt;</description>
	<pubDate>Thu, 03 Sep 2026 07:12:34 +0000</pubDate>
</item>
<item>
	<title>Graham Dumpleton: Recording calls with wrapture</title>
	<guid>https://grahamdumpleton.me/posts/2026/09/recording-calls-with-wrapture/</guid>
	<link>https://grahamdumpleton.me/posts/2026/09/recording-calls-with-wrapture/</link>
	<description>&lt;p&gt;In &lt;a href=&quot;https://grahamdumpleton.me/posts/2026/09/unit-testing-with-wrapture/&quot;&gt;unit testing with wrapture&lt;/a&gt; the tests leaned on a timeline and a tape to assert on what happened, and I skipped over what those actually are. This post is about the recording side of wrapture: what gets recorded, what one event holds, how a test reads the record back, and the whole-tape views that answer questions about the flow between calls rather than about any one of them.&lt;/p&gt;
&lt;p&gt;The example is a resource leak, because it is the kind of bug the recording model was made for. Code that acquires a connection has to release it on every path out: the normal return, the early return, and the exception. The path that forgets is the one nobody looks at, and it does not fail. Nothing raises, nothing returns the wrong value, the test passes, and the pool runs dry a week later in production. The failure is an absence, and asserting on an absence needs a record of what did happen, on the real objects, including objects minted mid-call that the test never held.&lt;/p&gt;
&lt;h2&gt;The code under test&lt;/h2&gt;
&lt;p&gt;A stand-in for any pooled resource. &lt;code&gt;Database.connect()&lt;/code&gt; mints a &lt;code&gt;Connection&lt;/code&gt;, and a connection answers queries until &lt;code&gt;close()&lt;/code&gt; sets its &lt;code&gt;closed&lt;/code&gt; flag:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class Connection:
    def __init__(self, number):
        self.number = number
        self.closed = False

    def execute(self, sql):
        if self.closed:
            raise RuntimeError(&amp;quot;connection is closed&amp;quot;)
        return [(1, &amp;quot;widget&amp;quot;)] if &amp;quot;id = 1&amp;quot; in sql else []

    def close(self):
        self.closed = True

    def __repr__(self):
        return f&amp;quot;&amp;lt;Connection {self.number}&amp;gt;&amp;quot;


class Database:
    def __init__(self):
        self.issued = 0

    def connect(self):
        self.issued += 1
        return Connection(self.issued)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The repository is where the bug lives. &lt;code&gt;count()&lt;/code&gt; releases in a &lt;code&gt;finally&lt;/code&gt;, so it is safe on every path. &lt;code&gt;find()&lt;/code&gt; releases only when a row was found; the not-found early return leaks its connection:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class Repository:
    def __init__(self, database):
        self.database = database

    def count(self, table):
        connection = self.database.connect()
        try:
            return len(connection.execute(f&amp;quot;SELECT * FROM {table}&amp;quot;))
        finally:
            connection.close()

    def find(self, table, key):
        connection = self.database.connect()
        rows = connection.execute(f&amp;quot;SELECT * FROM {table} WHERE id = {key}&amp;quot;)
        if not rows:
            return None
        connection.close()
        return rows[0]


def report(repository, keys):
    found = [repository.find(&amp;quot;products&amp;quot;, key) for key in keys]
    return repository.count(&amp;quot;products&amp;quot;), [row for row in found if row]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Running &lt;code&gt;report(Repository(Database()), [1, 2])&lt;/code&gt; returns &lt;code&gt;(0, [(1, 'widget')])&lt;/code&gt;, which is correct. Nothing about that result says a connection was left open.&lt;/p&gt;
&lt;p&gt;The usual way to test this is a hand-written fake &lt;code&gt;Database&lt;/code&gt; whose &lt;code&gt;connect()&lt;/code&gt; appends to a list, with connections that flip a flag, and a test that walks the list. It works, but it tests a substitute. The real classes never run, the fake has to be kept in step with them, and every acquiring class in the codebase needs its own. The record you want is of the real calls.&lt;/p&gt;
&lt;h2&gt;The timeline and the tape&lt;/h2&gt;
&lt;p&gt;Bind &lt;code&gt;connect&lt;/code&gt; on &lt;code&gt;Database&lt;/code&gt; and &lt;code&gt;close&lt;/code&gt; on &lt;code&gt;Connection&lt;/code&gt;, and record both onto one tape. Neither binding has any behaviour configured, so they observe and nothing else:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;connect = wrapture.binding(Database, &amp;quot;connect&amp;quot;)
close = wrapture.binding(Connection, &amp;quot;close&amp;quot;)

with wrapture.timeline(connect, close) as tape:
    report(Repository(Database()), [1, 2])

print(tape.tree())
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;__main__:Database.connect()  -&amp;gt; &amp;lt;Connection 1&amp;gt;
__main__:Connection.close()  -&amp;gt; None
__main__:Database.connect()  -&amp;gt; &amp;lt;Connection 2&amp;gt;
__main__:Database.connect()  -&amp;gt; &amp;lt;Connection 3&amp;gt;
__main__:Connection.close()  -&amp;gt; None
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three acquisitions, two releases, and reading down the tape you can already see which one has no partner.&lt;/p&gt;
&lt;p&gt;The two words are two views of one thing. The timeline is the scope: &lt;code&gt;with wrapture.timeline(...)&lt;/code&gt; opens it, the bindings handed to it are applied on entry and removed on exit, and while it is open every call through every applied binding records an event. The tape is what the scope holds. Bindings applied by other means, a fixture or an outer &lt;code&gt;with&lt;/code&gt;, record onto an open tape as well, and a binding applied with no timeline open records nothing and costs almost nothing beyond wrapt's own dispatch, so leaving bindings applied and only occasionally recording is a supported pattern rather than a mistake.&lt;/p&gt;
&lt;p&gt;Notice that &lt;code&gt;close&lt;/code&gt; is bound on the &lt;code&gt;Connection&lt;/code&gt; class, not on any connection object. The connections do not exist when the test starts; &lt;code&gt;connect()&lt;/code&gt; mints them mid-call. A binding on the class wraps the method for every instance, present and future, which is exactly what covers objects a factory hands out. A mock injected through a seam cannot see those objects at all.&lt;/p&gt;
&lt;h2&gt;What one event holds&lt;/h2&gt;
&lt;p&gt;Each call through a binding inside the scope records one event, and an event is a good deal richer than a mock's call record. The fields a test typically reads are &lt;code&gt;path&lt;/code&gt;, the fully qualified location in &lt;code&gt;module:qualname&lt;/code&gt; form; &lt;code&gt;instance&lt;/code&gt;, the object the method was called on; &lt;code&gt;arguments&lt;/code&gt;, the call normalised against the real signature with defaults applied, so &lt;code&gt;charge(500)&lt;/code&gt; and &lt;code&gt;charge(amount=500)&lt;/code&gt; record identically; &lt;code&gt;result&lt;/code&gt;, the real return value, or &lt;code&gt;exception&lt;/code&gt; when the call raised instead; and &lt;code&gt;seq&lt;/code&gt;, &lt;code&gt;parent_id&lt;/code&gt; and &lt;code&gt;depth&lt;/code&gt;, which place the event in the call tree. There are timings too, &lt;code&gt;started&lt;/code&gt; and &lt;code&gt;duration&lt;/code&gt;, with recording's own bookkeeping excluded from the figure.&lt;/p&gt;
&lt;p&gt;Because the values are real, they can be compared across events. A &lt;code&gt;connect&lt;/code&gt; event's &lt;code&gt;result&lt;/code&gt; is the connection it minted, and a &lt;code&gt;close&lt;/code&gt; event's &lt;code&gt;instance&lt;/code&gt; is the connection it was called on, so the leaked connections are the difference between the two sets:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;with wrapture.timeline(connect, close):
    report(Repository(Database()), [1, 2])

    acquired = {event.result for event in connect.events}
    released = {event.instance for event in close.events}

    print(acquired - released)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;{&amp;lt;Connection 2&amp;gt;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is the whole question answered, and it needed nothing from the repository. Events record what actually flowed, behaviour included: a call stubbed with &lt;code&gt;returns()&lt;/code&gt; records the stubbed result, a failure injected with &lt;code&gt;raises()&lt;/code&gt; records that exception, and when &lt;code&gt;transforms_args()&lt;/code&gt; rewrote the arguments the event keeps both the arguments as the caller sent them and the ones the real method received, which no substitution-based tool can record because replacing a function discards what it would have been called with.&lt;/p&gt;
&lt;h2&gt;Filters narrow, assertions conclude&lt;/h2&gt;
&lt;p&gt;A binding's &lt;code&gt;events&lt;/code&gt; property is a filterable view over the tape for that one binding, and it works inside the &lt;code&gt;with&lt;/code&gt; block after the code under test has run. One naming rule holds across the whole package: a method whose name starts with &lt;code&gt;assert_&lt;/code&gt; raises immediately, one starting with &lt;code&gt;expect_&lt;/code&gt; declares and is checked when the scope closes, and everything else returns data. A mistyped assertion name is therefore an &lt;code&gt;AttributeError&lt;/code&gt; rather than the silent pass mock's &lt;code&gt;assert_calld_once&lt;/code&gt; was famous for.&lt;/p&gt;
&lt;p&gt;Filters chain and never raise. &lt;code&gt;with_args(amount=500)&lt;/code&gt; keeps calls whose normalised arguments include the given values, &lt;code&gt;with_instance(obj)&lt;/code&gt; keeps calls made on exactly that object by identity, &lt;code&gt;raising(TimeoutError)&lt;/code&gt; keeps calls that raised, &lt;code&gt;returning(value)&lt;/code&gt; keeps calls that returned it, and &lt;code&gt;matching(predicate)&lt;/code&gt; is the escape hatch. Assertions then conclude: &lt;code&gt;assert_never()&lt;/code&gt;, &lt;code&gt;assert_once()&lt;/code&gt;, &lt;code&gt;assert_times(n)&lt;/code&gt;, &lt;code&gt;assert_at_least(n)&lt;/code&gt; and &lt;code&gt;assert_at_most(n)&lt;/code&gt;. Each returns the log on success so a passing assertion can keep chaining, and each prints the events it looked at on failure. Asserting three closes when there were two gives:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;AssertionError: expected exactly 3 event(s), got 2
&amp;lt;EventLog __main__:Connection.close: 2 event(s)&amp;gt;
    __main__:Connection.close()
    __main__:Connection.close()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An assertion is written where it runs. An expectation is the same claim declared on the binding up front, before the run, and verified when the timeline exits:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;close = wrapture.binding(Connection, &amp;quot;close&amp;quot;).expect_times(3)

with wrapture.timeline(connect, close):
    report(Repository(Database()), [1, 2])
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;ExpectationNotMetError: declared expectation on __main__:Connection.close not met: expected exactly 3 event(s), got 2
&amp;lt;EventLog __main__:Connection.close: 2 event(s)&amp;gt;
    __main__:Connection.close()
    __main__:Connection.close()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ExpectationNotMetError&lt;/code&gt; derives from &lt;code&gt;AssertionError&lt;/code&gt;, so test frameworks report it as a failure. Expectations read as a contract at the top of the test with the body free of bookkeeping, and an expectation with nothing recording is an error rather than a pass. Verification is skipped when the block itself raised, since the in-flight failure is the real cause and a verification error on top would bury it.&lt;/p&gt;
&lt;h2&gt;The tree names the culprit&lt;/h2&gt;
&lt;p&gt;Counting says something leaked, and pairing says what. To say who, add the repository methods to the timeline. The tape then nests each acquire and release under the method that made it, and &lt;code&gt;tape.children_of()&lt;/code&gt; walks the tree, so a root whose children include a &lt;code&gt;connect&lt;/code&gt; but no &lt;code&gt;close&lt;/code&gt; names itself:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;find = wrapture.binding(Repository, &amp;quot;find&amp;quot;)
count = wrapture.binding(Repository, &amp;quot;count&amp;quot;)

with wrapture.timeline(find, count, connect, close) as tape:
    report(Repository(Database()), [1, 2])

    print(tape.tree())

    for caller in tape.roots():
        paths = [child.path for child in tape.children_of(caller)]
        if &amp;quot;__main__:Connection.close&amp;quot; not in paths:
            print(&amp;quot;leaked by&amp;quot;, caller)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;__main__:Repository.find(table='products', key=1)  -&amp;gt; (1, 'widget')
  __main__:Database.connect()  -&amp;gt; &amp;lt;Connection 1&amp;gt;
  __main__:Connection.close()  -&amp;gt; None
__main__:Repository.find(table='products', key=2)  -&amp;gt; None
  __main__:Database.connect()  -&amp;gt; &amp;lt;Connection 2&amp;gt;
__main__:Repository.count(table='products')  -&amp;gt; 0
  __main__:Database.connect()  -&amp;gt; &amp;lt;Connection 3&amp;gt;
  __main__:Connection.close()  -&amp;gt; None
leaked by __main__:Repository.find(table='products', key=2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The tree shows the bug as it happened. &lt;code&gt;find()&lt;/code&gt; with a key that matched released its connection, &lt;code&gt;find()&lt;/code&gt; with a key that did not match never called &lt;code&gt;close()&lt;/code&gt;, and &lt;code&gt;count()&lt;/code&gt; released on the way out of its &lt;code&gt;finally&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;When the method is long, or acquires in several places, you want the line rather than the method. Stack capture on the acquire binding records the calling frame with each event, priced per binding so only the acquire pays for it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;connect = wrapture.binding(Database, &amp;quot;connect&amp;quot;, stack=&amp;quot;caller&amp;quot;)

with wrapture.timeline(connect, close):
    report(Repository(Database()), [1, 2])

    released = {event.instance for event in close.events}
    for event in connect.events:
        if event.result not in released:
            frame = wrapture.stack_frames(event.stack)[0]
            print(f&amp;quot;{event.result} acquired at line {frame.lineno} in {frame.function}, never released&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Connection 2&amp;gt; acquired at line 40 in Repository.find, never released
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Order across bindings&lt;/h2&gt;
&lt;p&gt;Per-binding logs answer questions about one call site; the tape answers questions about the flow between them. &lt;code&gt;tape.assert_order(connect, close)&lt;/code&gt; is a subsequence check across any bindings: other events may appear before, between and after, and only the relative order of the named bindings' events matters. A step can also be a filtered log, which is how to say which call, so &lt;code&gt;tape.assert_order(charge.events.raising(TimeoutError), refund)&lt;/code&gt; reads as &amp;quot;the refund came after the charge that timed out&amp;quot;. &lt;code&gt;consecutive=True&lt;/code&gt; requires the steps to match a consecutive run with nothing of those bindings' in between, and &lt;code&gt;exact=True&lt;/code&gt; requires those bindings' events to be exactly the steps, which are mock's &lt;code&gt;assert_has_calls&lt;/code&gt; and &lt;code&gt;mock_calls ==&lt;/code&gt; respectively, except that they work across bindings instead of within one mock.&lt;/p&gt;
&lt;p&gt;On failure the message names where the walk stalled and prints the actual timeline, which reads far better than a list diff. Asserting a close before a connect on a run that only leaked:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;AssertionError: expected order not satisfied; stalled waiting for __main__:Database.connect (position 2 of 2)
  actual timeline:
    __main__:Repository.find(table='products', key=2)
    __main__:Database.connect()
    __main__:Repository.count(table='products')
    __main__:Database.connect()
    __main__:Connection.close()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Scoping instead of resetting&lt;/h2&gt;
&lt;p&gt;A tape is never cleared. Where a mock suite reaches for &lt;code&gt;reset_mock()&lt;/code&gt; to discard setup calls before the act step, wrapture opens the timeline around the part that counts. Timelines nest, and an inner &lt;code&gt;timeline()&lt;/code&gt; with no arguments records only what happens inside it while the outer one keeps the whole run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;with wrapture.timeline(connect, close) as whole:
    repository = Repository(Database())
    repository.count(&amp;quot;products&amp;quot;)                # lands on `whole` only

    with wrapture.timeline() as act:
        repository.find(&amp;quot;products&amp;quot;, 1)
        connect.events.assert_once()            # the act step alone
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inside the inner block &lt;code&gt;connect.events&lt;/code&gt; reads the innermost tape, so the count is one even though the outer tape holds four events. The same scoping is how a phased test keeps each phase's counts separate, one timeline per phase, with the same bindings applied on entry and removed on exit each time. The second phase can then state &lt;code&gt;assert_never()&lt;/code&gt; outright, where one cumulative tape could only say the count is still one.&lt;/p&gt;
&lt;h2&gt;Messages and phases as events&lt;/h2&gt;
&lt;p&gt;Calls are not the only thing that records. An attribute binding records reads and writes of an attribute as &lt;code&gt;get&lt;/code&gt; and &lt;code&gt;set&lt;/code&gt; events on the same tape, which for this example means the &lt;code&gt;closed&lt;/code&gt; flag can be watched directly rather than inferred from &lt;code&gt;close()&lt;/code&gt; being called. That is a subject for a later post. Two other event producers are worth knowing about now, because they change what a test can pin an assertion to.&lt;/p&gt;
&lt;p&gt;The first is log capture. &lt;code&gt;capture_logs()&lt;/code&gt; records standard library logging onto the tape as events of kind &lt;code&gt;&amp;quot;log&amp;quot;&lt;/code&gt;, selected by logger name pattern and level, and it applies like a binding so &lt;code&gt;timeline()&lt;/code&gt; accepts it alongside them. Give the repository a warning when nothing is found, and the message lands inside the call that logged it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;logs = wrapture.capture_logs(&amp;quot;myapp.*&amp;quot;)

with wrapture.timeline(find, connect, close, logs) as tape:
    report(Repository(Database()), [1, 2])

    print(tape.tree())

    warning = logs.events.at_level(&amp;quot;WARNING&amp;quot;).with_message(&amp;quot;*no row*&amp;quot;).assert_once().first
    assert tape.parent_of(warning) is find.events.with_args(key=2).first
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;__main__:Repository.find(table='products', key=1)  -&amp;gt; (1, 'widget')
  __main__:Database.connect()  -&amp;gt; &amp;lt;Connection 1&amp;gt;
  __main__:Connection.close()  -&amp;gt; None
__main__:Repository.find(table='products', key=2)  -&amp;gt; None
  __main__:Database.connect()  -&amp;gt; &amp;lt;Connection 2&amp;gt;
  log myapp.repo WARNING 'no row in products with id 2'
__main__:Database.connect()  -&amp;gt; &amp;lt;Connection 3&amp;gt;
__main__:Connection.close()  -&amp;gt; None
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That last assertion is the one pytest's &lt;code&gt;caplog&lt;/code&gt; has no words for: the warning was logged by this call, not merely somewhere during the test. Capture sits at &lt;code&gt;Logger.handle&lt;/code&gt;, so it hears each record once on the logger that emitted it, before propagation and regardless of handler configuration, and nothing the application configured is touched.&lt;/p&gt;
&lt;p&gt;The second is a block. &lt;code&gt;wrapture.block(name)&lt;/code&gt; is a context manager the code, or the test, uses to declare a stretch of code as one event, with everything recorded inside it nested underneath. In a test body it names the phases of an integration test so that &amp;quot;the events during the second request&amp;quot; stops being an exercise in parent-chasing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;with wrapture.timeline(connect, close) as tape:
    repository = Repository(Database())

    with wrapture.block(&amp;quot;lookups&amp;quot;):
        repository.find(&amp;quot;products&amp;quot;, 1)
        repository.find(&amp;quot;products&amp;quot;, 2)

    with wrapture.block(&amp;quot;summary&amp;quot;):
        repository.count(&amp;quot;products&amp;quot;)

    lookups = tape.blocks(&amp;quot;lookups&amp;quot;).assert_once().first
    tape.within(lookups).for_binding(close).assert_once()
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;block: lookups
  __main__:Database.connect()  -&amp;gt; &amp;lt;Connection 1&amp;gt;
  __main__:Connection.close()  -&amp;gt; None
  __main__:Database.connect()  -&amp;gt; &amp;lt;Connection 2&amp;gt;
block: summary
  __main__:Database.connect()  -&amp;gt; &amp;lt;Connection 3&amp;gt;
  __main__:Connection.close()  -&amp;gt; None
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;tape.within(event)&lt;/code&gt; scopes the whole query surface to one block's contents, so an ordering assertion on the view never sees an event outside it. In application code the same marker is inert when nothing is listening, so it can stay in production code permanently, which is what makes the same block a span when the events are going to a tracing backend rather than a test.&lt;/p&gt;
&lt;h2&gt;As a pytest test&lt;/h2&gt;
&lt;p&gt;In a test the pairing becomes the assertion, and the failure message carries the leaked connections and where each was acquired. &lt;code&gt;close&lt;/code&gt; is given a declared expectation of at least one call, so a path that acquires nothing at all cannot pass by accident:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def test_find_releases_its_connection():
    connect = wrapture.binding(Database, &amp;quot;connect&amp;quot;, stack=&amp;quot;caller&amp;quot;)
    close = wrapture.binding(Connection, &amp;quot;close&amp;quot;).expect_at_least(1)

    with wrapture.timeline(connect, close):
        report(Repository(Database()), [1, 2])

        released = {event.instance for event in close.events}
        leaked = [
            (event.result, wrapture.stack_frames(event.stack)[0])
            for event in connect.events
            if event.result not in released
        ]

        assert not leaked, f&amp;quot;connections left open: {leaked}&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The test fails today, naming &lt;code&gt;&amp;lt;Connection 2&amp;gt;&lt;/code&gt; and the frame inside &lt;code&gt;find()&lt;/code&gt;. Fix the early return with a &lt;code&gt;finally&lt;/code&gt; and it passes. With the pytest plugin enabled the tape's tree is attached to the failure report as well, so the output shows what ran rather than only the assertion that tripped.&lt;/p&gt;
&lt;h2&gt;What's next&lt;/h2&gt;
&lt;p&gt;Everything in this post recorded real calls with the bindings doing nothing but watch. The next post is about the other direction, changing what a call does, and specifically about behaviour that changes over time as the code under test keeps calling, which is what retry logic and circuit breakers need from a test.&lt;/p&gt;</description>
	<pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
</item>
<item>
	<title>Django Weblog: Django bugfix release issued: 6.1.1</title>
	<guid>https://www.djangoproject.com/weblog/2026/sep/02/bugfix-releases/</guid>
	<link>https://www.djangoproject.com/weblog/2026/sep/02/bugfix-releases/</link>
	<description>&lt;p&gt;Today we've issued the
&lt;a href=&quot;https://docs.djangoproject.com/en/stable/releases/6.1.1/&quot;&gt;6.1.1&lt;/a&gt;
bugfix release.&lt;/p&gt;
&lt;p&gt;The release package and checksums are available from
&lt;a href=&quot;http://www.djangoproject.com/download/&quot;&gt;our downloads page&lt;/a&gt;, as well as from the Python Package Index.&lt;/p&gt;
&lt;p&gt;The PGP key ID used for this release is Jacob Walls: &lt;a href=&quot;https://github.com/jacobtylerwalls.gpg&quot;&gt;131403F4D16D8DC7&lt;/a&gt;&lt;/p&gt;</description>
	<pubDate>Wed, 02 Sep 2026 17:30:00 +0000</pubDate>
</item>
<item>
	<title>Tryton News: Tryton News September 2026</title>
	<guid>https://discuss.tryton.org/t/tryton-news-september-2026/9356</guid>
	<link>https://discuss.tryton.org/t/tryton-news-september-2026/9356</link>
	<description>&lt;div&gt;  &lt;/div&gt;

&lt;p&gt;&lt;/p&gt;&lt;div class=&quot;lightbox-wrapper&quot;&gt;&lt;a class=&quot;lightbox&quot; href=&quot;https://discuss-cdn.tryton.org/uploads/default/original/2X/c/cc517aeb55ed7903b396da24a1e85618ba34270a.jpeg&quot; title=&quot;Photo: Ron Lach, Pexels&quot;&gt;&lt;img src=&quot;https://discuss-cdn.tryton.org/uploads/default/optimized/2X/c/cc517aeb55ed7903b396da24a1e85618ba34270a_2_333x500.jpeg&quot; alt=&quot;Two colleagues focusing on a post production video editing task in a dark studio environment.&quot; title=&quot;Photo: Ron Lach, Pexels&quot; width=&quot;333&quot; height=&quot;500&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;September brings a balance of trytond internals and business-module refinements. The server now retries queued tasks that were dropped because a worker died, enforces request timeouts for the whole request, and exposes routes so RPC endpoints can be registered declaratively. On the user side, European VAT numbers are now validated in the background, stock periods close themselves, and the IBAN editor formats the number as it is typed-in. All of this builds on &lt;a href=&quot;https://discuss.tryton.org/t/tryton-release-8-0/&quot;&gt;our last LTS release 8.0&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For an in depth overview of all the &lt;a href=&quot;https://bugs.tryton.org/&quot;&gt;Tryton issues please take a look at our issue tracker&lt;/a&gt; or see the issues and merge requests &lt;a href=&quot;https://code.tryton.org/tryton/-/labels&quot;&gt;filtered by label&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;heading--user&quot;&gt;Changes for the User&lt;/h2&gt;
&lt;h3 id=&quot;heading--accounting&quot;&gt;Accounting, Invoicing and Payments&lt;/h3&gt;
&lt;p&gt;The &lt;a href=&quot;https://bugs.tryton.org/14930&quot;&gt;automatic VIES check replaces the previous wizard&lt;/a&gt; for European VAT numbers. A background task validates new and modified EU VAT identifiers and refreshes them once the configured validity period has expired. The validity of the identifier is also &lt;a href=&quot;https://bugs.tryton.org/14931&quot;&gt;checked when posting an invoice&lt;/a&gt;, so a stale or invalid VAT number is caught before the invoice is sent. The last validation-state and validation-date is displayed in the identifier view.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://bugs.tryton.org/14710&quot;&gt;&lt;code&gt;party required&lt;/code&gt; setting can no longer be changed on an account that already has account moves&lt;/a&gt;. This avoids mixing moves with and without a party on the same account, which used to confuse later reports.&lt;/p&gt;
&lt;p&gt;The redundant prefix is &lt;a href=&quot;https://code.tryton.org/tryton/-/commit/025f022060ab2a2a6169502f60c9ed138844a658&quot;&gt;dropped from the statement and payment journals actions&lt;/a&gt;. So the menus for accounting, statements and payments no longer repeat the “statement” or “payment” word.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://code.tryton.org/tryton/-/commit/c883d23dcb027a8075cc6debe2e8908ab7015e11&quot;&gt;IBAN of a bank account on a party is now formatted with spaces between groups of four characters on changing the field&lt;/a&gt;. This makes it easier to check the number was entered correctly.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://code.tryton.org/tryton/-/commit/ee6db5164d0d32d4ce8d48ab69f2ebb2112ad3e8&quot;&gt;version of the Stripe API used by the payment gateway is updated&lt;/a&gt; to the latest one.&lt;/p&gt;
&lt;h3 id=&quot;heading--stock&quot;&gt;Stock, Production and Shipments&lt;/h3&gt;
&lt;p&gt;Stock periods can now be &lt;a href=&quot;https://code.tryton.org/tryton/-/commit/9539955e9debd2265a6b4f8b79728192b7e99368&quot;&gt;created and closed automatically&lt;/a&gt; by two scheduled tasks, &lt;a href=&quot;https://docs.tryton.org/latest/modules-stock/usage/index.html#using-stock-periods&quot;&gt;automating the manual steps&lt;/a&gt; at the start and end of each period.&lt;/p&gt;
&lt;p&gt;On a stock move that is linked to a shipment but opened outside the shipment form, the &lt;a href=&quot;https://bugs.tryton.org/12564&quot;&gt;from and to locations are now read-only&lt;/a&gt;. This prevents the location domain inherited from the shipment from being bypassed. The domain is also &lt;a href=&quot;https://code.tryton.org/tryton/-/commit/1e6c3426d11da03d511c69357a7e25b0ec1ded17&quot;&gt;enforced on stock moves&lt;/a&gt;, so each move matches at least one of the shipment’s two move fields: &lt;em&gt;incoming moves&lt;/em&gt; or &lt;em&gt;inventory moves&lt;/em&gt;.&lt;/p&gt;
&lt;h3 id=&quot;heading--ui&quot;&gt;User Interface&lt;/h3&gt;
&lt;p&gt;In the SAO client, &lt;a href=&quot;https://bugs.tryton.org/15025&quot;&gt;tabs now scroll horizontally&lt;/a&gt; when they overflow the tab list. The scrolling is smooth for a nicer effect when adding a new tab.&lt;/p&gt;
&lt;h2 id=&quot;heading--new-releases&quot;&gt;New Releases&lt;/h2&gt;
&lt;p&gt;We released bug fixes for the currently maintained &lt;a href=&quot;https://discuss.tryton.org/t/release-process/395&quot;&gt;long term support series&lt;/a&gt; &lt;a href=&quot;https://code.tryton.org/tryton/-/commits/branch/8.0&quot;&gt;8.0&lt;/a&gt;, &lt;a href=&quot;https://code.tryton.org/tryton/-/commits/branch/7.8&quot;&gt;7.8&lt;/a&gt;, and &lt;a href=&quot;https://code.tryton.org/tryton/-/commits/branch/7.0&quot;&gt;7.0&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;heading--developer&quot;&gt;Changes for Implementers and Developers&lt;/h2&gt;
&lt;p&gt;A mixin can now be &lt;a href=&quot;https://code.tryton.org/tryton/-/commit/33139eb55d0036e7e7c5b538312754994006dd2c&quot;&gt;added to the Database and TableHandler&lt;/a&gt; from the &lt;a href=&quot;https://docs.tryton.org/latest/server/topics/configuration.html#database-mixins&quot;&gt;configuration&lt;/a&gt;. This is used by the gis module to register PostGIS as a backend mixin.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://code.tryton.org/tryton/-/commit/571adc3fe3f538b5c89daac271d6ddba5b30fae8&quot;&gt;Pool now exposes routes&lt;/a&gt;, so RPC endpoints can be registered declaratively using a Router.&lt;/p&gt;
&lt;p&gt;The trytond request &lt;a href=&quot;https://bugs.tryton.org/14946&quot;&gt;timeout is now enforced for the whole request&lt;/a&gt;, not just for individual queries. &lt;a href=&quot;https://docs.python.org/3/library/threading.html#timer-objects&quot;&gt;A threading timer&lt;/a&gt; injects an exception into the running thread when the timeout expires.&lt;/p&gt;
&lt;p&gt;Queued tasks that were dequeued but never finished, because the worker was killed, are now &lt;a href=&quot;https://bugs.tryton.org/14949&quot;&gt;retried by a scheduled task&lt;/a&gt;. The retry uses the &lt;code&gt;finished_at&lt;/code&gt; timestamp and the task lock to know which tasks are still outstanding.&lt;/p&gt;
&lt;p&gt;&lt;small&gt;Initial draft powered by Minimax-M3. Curated and finalised by human hands.&lt;/small&gt;&lt;/p&gt;
            &lt;p&gt;&lt;small&gt;1 post - 1 participant&lt;/small&gt;&lt;/p&gt;
            &lt;p&gt;&lt;a href=&quot;https://discuss.tryton.org/t/tryton-news-september-2026/9356&quot;&gt;Read full topic&lt;/a&gt;&lt;/p&gt;</description>
	<pubDate>Wed, 02 Sep 2026 06:00:54 +0000</pubDate>
</item>
<item>
	<title>Python GUIs: Are there any built-in QIcons? — Using built-in icons for your apps.</title>
	<guid>https://www.pythonguis.com/faq/built-in-qicons-pyqt/</guid>
	<link>https://www.pythonguis.com/faq/built-in-qicons-pyqt/</link>
	<description>&lt;p&gt;In the tutorials on this site and in &lt;a href=&quot;https://www.pythonguis.com/books/&quot;&gt;my books&lt;/a&gt; I recommend using the &lt;a href=&quot;https://p.yusukekamiyamane.com/&quot;&gt;fugue icons set&lt;/a&gt;. This is a &lt;em&gt;free&lt;/em&gt; set of icons from Yusuke Kamiyamane, a freelance designer from Tokyo. The set contains 3,570 icons and is a great way to add some nice visual touches to your application without much hassle.&lt;/p&gt;
&lt;p&gt;But this isn't the only icon set available, and there's another option you may not know about. Read on for details.&lt;/p&gt;
&lt;p&gt;Veronica asked:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Are there any built-in icons with PyQt5? I have searched the web and it seems like there are some but I can't find any examples of them being used. Does it depend on the situation? If so, then in which cases can I use an icon without downloading it first?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;First we need to clarify what is meant by &lt;em&gt;built-in&lt;/em&gt; icons &amp;mdash; it can mean two different things depending on context &amp;mdash; either Qt built-in, or system built-in (Linux only). I'll start with the Qt built-ins as that's cross-platform (they're available on Windows, macOS and Linux).&lt;/p&gt;
&lt;h2 id=&quot;qt-standard-icons-qstyle-standardpixmap&quot;&gt;Qt Standard Icons (QStyle StandardPixmap)&lt;/h2&gt;
&lt;p&gt;Qt ships with a small set of standard icons you can use in any of your applications for common actions. These built-in icons are accessed through the &lt;code&gt;QStyle.StandardPixmap&lt;/code&gt; enum and retrieved using &lt;code&gt;style().standardIcon()&lt;/code&gt;. They're available on all platforms &amp;mdash; Windows, macOS, and Linux &amp;mdash; making them a convenient choice when you need common UI icons without bundling external assets.&lt;/p&gt;
&lt;p&gt;The following Python script displays all the built-in Qt standard icons in a grid layout:&lt;/p&gt;
&lt;div class=&quot;tabbed-area multicode&quot;&gt;&lt;ul class=&quot;tabs&quot;&gt;&lt;li class=&quot;tab-link current&quot;&gt;PyQt5&lt;/li&gt;
&lt;li class=&quot;tab-link&quot;&gt;PyQt6&lt;/li&gt;
&lt;li class=&quot;tab-link&quot;&gt;PySide2&lt;/li&gt;
&lt;li class=&quot;tab-link&quot;&gt;PySide6&lt;/li&gt;&lt;/ul&gt;&lt;div class=&quot;tab-content current code-block-outer&quot; id=&quot;444314d477c24b6b82e0cdc8496c9fa7&quot;&gt;
&lt;div class=&quot;code-block&quot;&gt;
&lt;span class=&quot;code-block-language code-block-python&quot;&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;import sys

from PyQt5.QtWidgets import QApplication, QGridLayout, QPushButton, QStyle, QWidget


class Window(QWidget):
    def __init__(self):
        super().__init__()

        icons = sorted([attr for attr in dir(QStyle) if attr.startswith(&quot;SP_&quot;)])
        layout = QGridLayout()

        for n, name in enumerate(icons):
            btn = QPushButton(name)

            pixmapi = getattr(QStyle, name)
            icon = self.style().standardIcon(pixmapi)
            btn.setIcon(icon)
            layout.addWidget(btn, n // 4, n % 4)

        self.setLayout(layout)


app = QApplication(sys.argv)

w = Window()
w.show()

app.exec_()


&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;tab-content code-block-outer&quot; id=&quot;9915a25cc557444c94cd5138072d5394&quot;&gt;
&lt;div class=&quot;code-block&quot;&gt;
&lt;span class=&quot;code-block-language code-block-python&quot;&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;import sys

from PyQt6.QtWidgets import (QApplication, QGridLayout, QPushButton, QStyle,
                             QWidget)


class Window(QWidget):
    def __init__(self):
        super().__init__()

        icons = sorted([attr for attr in dir(QStyle.StandardPixmap) if attr.startswith(&quot;SP_&quot;)])
        layout = QGridLayout()

        for n, name in enumerate(icons):
            btn = QPushButton(name)

            pixmapi = getattr(QStyle.StandardPixmap, name)
            icon = self.style().standardIcon(pixmapi)
            btn.setIcon(icon)
            layout.addWidget(btn, int(n/4), int(n%4))

        self.setLayout(layout)


app = QApplication(sys.argv)

w = Window()
w.show()

app.exec()

&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;tab-content code-block-outer&quot; id=&quot;13d0d3163bc24e15b11bc1ed0dcca580&quot;&gt;
&lt;div class=&quot;code-block&quot;&gt;
&lt;span class=&quot;code-block-language code-block-python&quot;&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;import sys

from PySide2.QtWidgets import QApplication, QGridLayout, QPushButton, QStyle, QWidget


class Window(QWidget):
    def __init__(self):
        super().__init__()

        icons = sorted([attr for attr in dir(QStyle) if attr.startswith(&quot;SP_&quot;)])
        layout = QGridLayout()

        for n, name in enumerate(icons):
            btn = QPushButton(name)

            pixmapi = getattr(QStyle, name)
            icon = self.style().standardIcon(pixmapi)
            btn.setIcon(icon)
            layout.addWidget(btn, n // 4, n % 4)

        self.setLayout(layout)


app = QApplication(sys.argv)

w = Window()
w.show()

app.exec_()

&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;tab-content code-block-outer&quot; id=&quot;da64910076324e54a62f31650a0129e6&quot;&gt;
&lt;div class=&quot;code-block&quot;&gt;
&lt;span class=&quot;code-block-language code-block-python&quot;&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;import sys

from PySide6.QtWidgets import QApplication, QGridLayout, QPushButton, QStyle, QWidget


class Window(QWidget):
    def __init__(self):
        super().__init__()

        icons = sorted(
            [attr for attr in dir(QStyle.StandardPixmap) if attr.startswith(&quot;SP_&quot;)]
        )
        layout = QGridLayout()

        for n, name in enumerate(icons):
            btn = QPushButton(name)

            pixmapi = getattr(QStyle, name)
            icon = self.style().standardIcon(pixmapi)
            btn.setIcon(icon)
            layout.addWidget(btn, n // 4, n % 4)

        self.setLayout(layout)


app = QApplication(sys.argv)

w = Window()
w.show()

app.exec()

&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;If you run this script you'll see the following window, listing all the available built-in Qt icons.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Qt's Built-in Standard Icons displayed in a grid using QStyle StandardPixmap&quot; src=&quot;https://www.pythonguis.com/static/faq/built-in-qicons-pyqt/icons-builtin.png&quot; width=&quot;900&quot; height=&quot;706&quot; /&gt;
&lt;em&gt;Qt's Built-in Icons &amp;mdash; all QStyle.StandardPixmap icons shown with their names&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;complete-list-of-qt-built-in-standard-icons-qstylestandardpixmap&quot;&gt;Complete List of Qt Built-in Standard Icons (QStyle.StandardPixmap)&lt;/h2&gt;
&lt;p&gt;The full table of all &lt;code&gt;QStyle.StandardPixmap&lt;/code&gt; icon names is below. You can use any of these in PyQt5, PyQt6, PySide2, or PySide6 applications.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;..&lt;/th&gt;
&lt;th&gt;..&lt;/th&gt;
&lt;th&gt;..&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SP_ArrowBack&lt;/td&gt;
&lt;td&gt;SP_DirIcon&lt;/td&gt;
&lt;td&gt;SP_MediaSkipBackward&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SP_ArrowDown&lt;/td&gt;
&lt;td&gt;SP_DirLinkIcon&lt;/td&gt;
&lt;td&gt;SP_MediaSkipForward&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SP_ArrowForward&lt;/td&gt;
&lt;td&gt;SP_DirOpenIcon&lt;/td&gt;
&lt;td&gt;SP_MediaStop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SP_ArrowLeft&lt;/td&gt;
&lt;td&gt;SP_DockWidgetCloseButton&lt;/td&gt;
&lt;td&gt;SP_MediaVolume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SP_ArrowRight&lt;/td&gt;
&lt;td&gt;SP_DriveCDIcon&lt;/td&gt;
&lt;td&gt;SP_MediaVolumeMuted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SP_ArrowUp&lt;/td&gt;
&lt;td&gt;SP_DriveDVDIcon&lt;/td&gt;
&lt;td&gt;SP_MessageBoxCritical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SP_BrowserReload&lt;/td&gt;
&lt;td&gt;SP_DriveFDIcon&lt;/td&gt;
&lt;td&gt;SP_MessageBoxInformation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SP_BrowserStop&lt;/td&gt;
&lt;td&gt;SP_DriveHDIcon&lt;/td&gt;
&lt;td&gt;SP_MessageBoxQuestion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SP_CommandLink&lt;/td&gt;
&lt;td&gt;SP_DriveNetIcon&lt;/td&gt;
&lt;td&gt;SP_MessageBoxWarning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SP_ComputerIcon&lt;/td&gt;
&lt;td&gt;SP_FileDialogBack&lt;/td&gt;
&lt;td&gt;SP_TitleBarCloseButton&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SP_CustomBase&lt;/td&gt;
&lt;td&gt;SP_FileDialogContentsView&lt;/td&gt;
&lt;td&gt;SP_TitleBarContextHelpButton&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SP_DesktopIcon&lt;/td&gt;
&lt;td&gt;SP_FileDialogDetailedView&lt;/td&gt;
&lt;td&gt;SP_TitleBarMaxButton&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SP_DialogApplyButton&lt;/td&gt;
&lt;td&gt;SP_FileDialogEnd&lt;/td&gt;
&lt;td&gt;SP_TitleBarMenuButton&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SP_DialogCancelButton&lt;/td&gt;
&lt;td&gt;SP_FileDialogInfoView&lt;/td&gt;
&lt;td&gt;SP_TitleBarMinButton&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SP_DialogCloseButton&lt;/td&gt;
&lt;td&gt;SP_FileDialogListView&lt;/td&gt;
&lt;td&gt;SP_TitleBarNormalButton&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SP_DialogDiscardButton&lt;/td&gt;
&lt;td&gt;SP_FileDialogNewFolder&lt;/td&gt;
&lt;td&gt;SP_TitleBarShadeButton&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SP_DialogHelpButton&lt;/td&gt;
&lt;td&gt;SP_FileDialogStart&lt;/td&gt;
&lt;td&gt;SP_TitleBarUnshadeButton&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SP_DialogNoButton&lt;/td&gt;
&lt;td&gt;SP_FileDialogToParent&lt;/td&gt;
&lt;td&gt;SP_ToolBarHorizontalExtensionButton&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SP_DialogOkButton&lt;/td&gt;
&lt;td&gt;SP_FileIcon&lt;/td&gt;
&lt;td&gt;SP_ToolBarVerticalExtensionButton&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SP_DialogResetButton&lt;/td&gt;
&lt;td&gt;SP_FileLinkIcon&lt;/td&gt;
&lt;td&gt;SP_TrashIcon&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SP_DialogSaveButton&lt;/td&gt;
&lt;td&gt;SP_MediaPause&lt;/td&gt;
&lt;td&gt;SP_VistaShield&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SP_DialogYesButton&lt;/td&gt;
&lt;td&gt;SP_MediaPlay&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SP_DirClosedIcon&lt;/td&gt;
&lt;td&gt;SP_MediaSeekBackward&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SP_DirHomeIcon&lt;/td&gt;
&lt;td&gt;SP_MediaSeekForward&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;how-to-use-a-specific-built-in-qicon-in-your-application&quot;&gt;How to Use a Specific Built-in QIcon in Your Application&lt;/h2&gt;
&lt;p&gt;In our script above to get the icons we're looking them up by name on the &lt;code&gt;QStyle&lt;/code&gt; object, using &lt;code&gt;getattr&lt;/code&gt; &amp;mdash; but this is only necessary so we can iterate over the list of names and display the icon next to their name. If you want a specific icon you can access it directly. For example, to use the critical message box icon:&lt;/p&gt;
&lt;div class=&quot;tabbed-area multicode&quot;&gt;&lt;ul class=&quot;tabs&quot;&gt;&lt;li class=&quot;tab-link current&quot;&gt;Others&lt;/li&gt;
&lt;li class=&quot;tab-link&quot;&gt;PyQt6&lt;/li&gt;&lt;/ul&gt;&lt;div class=&quot;tab-content current code-block-outer&quot; id=&quot;f917d8866630405bb7f355b6628f2c22&quot;&gt;
&lt;div class=&quot;code-block&quot;&gt;
&lt;span class=&quot;code-block-language code-block-python&quot;&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;pixmapi = QStyle.SP_MessageBoxCritical
icon = self.style().standardIcon(pixmapi)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;tab-content code-block-outer&quot; id=&quot;192ac4232d2c4a3f9b831069d6711e29&quot;&gt;
&lt;div class=&quot;code-block&quot;&gt;
&lt;span class=&quot;code-block-language code-block-python&quot;&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;pixmapi = QStyle.StandardPixmap.SP_MessageBoxCritical
icon = self.style().standardIcon(pixmapi)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p class=&quot;admonition admonition-note&quot;&gt;&lt;span class=&quot;admonition-kind&quot;&gt;&lt;i class=&quot;fas fa-sticky-note&quot;&gt;&lt;/i&gt;&lt;/span&gt;  In PyQt6 the flags must be accessed via &lt;code&gt;QStyle.StandardPixmap&lt;/code&gt;. In other versions, they are available on &lt;code&gt;QStyle&lt;/code&gt; itself.&lt;/p&gt;
&lt;p&gt;Once you have the &lt;code&gt;QIcon&lt;/code&gt; object, you can use it anywhere Qt expects an icon &amp;mdash; on buttons, toolbars, menus, window titles, and more.&lt;/p&gt;
&lt;h2 id=&quot;free-desktop-theme-icons-linux&quot;&gt;Free Desktop Theme Icons (Linux)&lt;/h2&gt;
&lt;p&gt;On Linux desktops there is something called the &lt;em&gt;Free Desktop Specification&lt;/em&gt; which defines standard names for icons for specific actions.&lt;/p&gt;
&lt;p&gt;If your application uses these specific icon names (and loads the icon from a &quot;theme&quot;) then on Linux your application will use the current icon set which is enabled on the desktop. The idea is to make all applications have the same look &amp;amp; feel while remaining user configurable.&lt;/p&gt;
&lt;h3&gt;Setting Theme Icons in Qt Designer&lt;/h3&gt;
&lt;p&gt;To use Free Desktop theme icons within Qt Designer you would select the drop-down and choose &quot;Set Icon From Theme...&quot;&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Setting an icon from theme in Qt Designer&quot; src=&quot;https://www.pythonguis.com/static/faq/built-in-qicons-pyqt/icontheme1.png&quot; width=&quot;972&quot; height=&quot;194&quot; /&gt;&lt;/p&gt;
&lt;p&gt;You then enter the &lt;em&gt;name&lt;/em&gt; of the icon you want to use, e.g. &lt;code&gt;document-new&lt;/code&gt; (the &lt;a href=&quot;https://specifications.freedesktop.org/icon-naming-spec/latest/ar01s04.html&quot;&gt;full list of valid names&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Entering icon theme name in Qt Designer&quot; src=&quot;https://www.pythonguis.com/static/faq/built-in-qicons-pyqt/icontheme2.png&quot; width=&quot;972&quot; height=&quot;194&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Setting Theme Icons in Python Code with QIcon.fromTheme()&lt;/h3&gt;
&lt;p&gt;If you're not using Qt Designer, you can set icons from a theme in your Python code using &lt;code&gt;QIcon.fromTheme()&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;
&lt;span class=&quot;code-block-language code-block-python&quot;&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;        icon = QtGui.QIcon.fromTheme(&quot;document-new&quot;)
        self.pushButton_n6.setIcon(icon)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;If you're developing a cross-platform Python GUI application you'll still need your own icons for Windows &amp;amp; macOS, but by using these theme names you can ensure that your app looks &lt;em&gt;native&lt;/em&gt; when run on Linux.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Does the &lt;code&gt;QIcon.fromTheme()&lt;/code&gt; method only work on Linux?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Qt themes work on all platforms, it's just that on Linux you get the theme for &lt;em&gt;free&lt;/em&gt;. On non-Linux platforms you have to define your own icon theme from scratch. However, this is only really worth doing if you want to have a Linux-native look &amp;mdash; for other use cases the &lt;code&gt;QResource&lt;/code&gt; system is simpler.&lt;/p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;
&lt;p&gt;There are two ways to use built-in icons in your PyQt or PySide applications without downloading external icon sets:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Qt Standard Icons (QStyle.StandardPixmap)&lt;/strong&gt; &amp;mdash; A cross-platform set of common UI icons built into Qt itself, accessible via &lt;code&gt;style().standardIcon()&lt;/code&gt;. These work on Windows, macOS, and Linux.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Free Desktop Theme Icons&lt;/strong&gt; &amp;mdash; Linux-specific system icons accessed via &lt;code&gt;QIcon.fromTheme()&lt;/code&gt; that match the user's current desktop theme for a native look and feel.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For most cross-platform PyQt6 or PySide6 projects, bundling a dedicated icon set like Fugue gives you the most control over your app's appearance. But for quick prototypes or platform-specific tools, Qt's built-in icons are a convenient and dependency-free option.&lt;/p&gt;
            &lt;p&gt;For an in-depth guide to building Python GUIs with PyQt5 see my book, &lt;a href=&quot;https://www.pythonguis.com/pyqt5-book/&quot;&gt;Create GUI Applications with Python &amp;amp; Qt5.&lt;/a&gt;&lt;/p&gt;</description>
	<pubDate>Wed, 02 Sep 2026 06:00:00 +0000</pubDate>
</item>
<item>
	<title>Python GUIs: Understanding QPainter Coordinates in PyQt6 — How the coordinate system works for drawing on canvases in PyQt6</title>
	<guid>https://www.pythonguis.com/faq/coordinates-on-qpainter/</guid>
	<link>https://www.pythonguis.com/faq/coordinates-on-qpainter/</link>
	<description>&lt;blockquote&gt;
&lt;p&gt;I really having trouble understanding the coordinate system used in &lt;code&gt;QPainter&lt;/code&gt;. Can you explain how this works?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If you've started drawing with &lt;code&gt;QPainter&lt;/code&gt; in PyQt6, you might have been surprised the first time you drew a line. You pass in coordinates like &lt;code&gt;(10, 10, 300, 200)&lt;/code&gt; and the result doesn't look quite like what you'd expect from a math class. That's because &lt;code&gt;QPainter&lt;/code&gt; uses a coordinate system where &lt;strong&gt;the origin (0, 0) is in the top-left corner&lt;/strong&gt; of the canvas, not the bottom-left.&lt;/p&gt;
&lt;p&gt;This catches a lot of people off guard, so in this tutorial we'll walk through exactly how QPainter coordinates work, how to visualize them, and how to convert between screen coordinates and the mathematical coordinate system you might be more familiar with.&lt;/p&gt;
&lt;h2 id=&quot;the-qpainter-coordinate-system&quot;&gt;The QPainter coordinate system&lt;/h2&gt;
&lt;p&gt;In most math courses, you learn to plot points on a Cartesian plane where &lt;code&gt;(0, 0)&lt;/code&gt; is at the bottom-left. The x-axis increases to the right, and the y-axis increases upward.&lt;/p&gt;
&lt;p&gt;QPainter (and most screen-based graphics systems) does things differently:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;(0, 0)&lt;/code&gt; is at the &lt;strong&gt;top-left&lt;/strong&gt; corner of the drawing surface.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;x-axis&lt;/strong&gt; increases to the &lt;strong&gt;right&lt;/strong&gt; (same as math).&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;y-axis&lt;/strong&gt; increases &lt;strong&gt;downward&lt;/strong&gt; (opposite of math).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This means that as your y value gets larger, you move &lt;em&gt;down&lt;/em&gt; the screen, not up. Here's a simple diagram to illustrate:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;
&lt;span class=&quot;code-block-language code-block-python&quot;&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;(0,0) &amp;amp;boxh&amp;amp;boxh&amp;amp;boxh&amp;amp;boxh&amp;amp;boxh&amp;amp;boxh&amp;amp;boxh&amp;amp;boxh&amp;amp;boxh&amp;amp;boxh&amp;amp;boxh&amp;amp;boxh&amp;amp;boxh&amp;amp;boxh&amp;amp;boxh► x increases
  &amp;amp;boxv
  &amp;amp;boxv
  &amp;amp;boxv
  &amp;amp;boxv
  ▼
  y increases
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;So when you call &lt;code&gt;painter.drawLine(10, 10, 300, 200)&lt;/code&gt;, you're drawing a line from a point near the top-left corner down to a point further right and further &lt;em&gt;down&lt;/em&gt; the canvas.&lt;/p&gt;
&lt;h2 id=&quot;seeing-it-in-action&quot;&gt;Seeing it in action&lt;/h2&gt;
&lt;p&gt;Let's draw a line and annotate the start and end points so you can see exactly where the coordinates land. This complete example creates a small window with a &lt;code&gt;QLabel&lt;/code&gt; displaying a &lt;code&gt;QPixmap&lt;/code&gt; that we draw onto.&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;
&lt;span class=&quot;code-block-language code-block-python&quot;&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;import sys

from PyQt6.QtCore import Qt
from PyQt6.QtGui import QPixmap, QPainter, QPen, QFont
from PyQt6.QtWidgets import QApplication, QLabel, QMainWindow


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle(&quot;QPainter Coordinates&quot;)

        canvas = QPixmap(400, 300)
        canvas.fill(Qt.white)

        painter = QPainter(canvas)

        # Draw the line.
        pen = QPen(Qt.blue, 2)
        painter.setPen(pen)
        painter.drawLine(10, 10, 300, 200)

        # Annotate the start point.
        pen = QPen(Qt.red, 6)
        painter.setPen(pen)
        painter.drawPoint(10, 10)

        painter.setPen(QPen(Qt.black))
        painter.setFont(QFont(&quot;Arial&quot;, 10))
        painter.drawText(20, 15, &quot;(10, 10)&quot;)

        # Annotate the end point.
        pen = QPen(Qt.red, 6)
        painter.setPen(pen)
        painter.drawPoint(300, 200)

        painter.setPen(QPen(Qt.black))
        painter.drawText(220, 220, &quot;(300, 200)&quot;)

        painter.end()

        label = QLabel()
        label.setPixmap(canvas)
        self.setCentralWidget(label)


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;Run this and you'll see a blue line drawn from near the top-left corner of the canvas down to a point lower and to the right. The red dots and labels mark each endpoint, making it clear that &lt;code&gt;(10, 10)&lt;/code&gt; is near the top-left and &lt;code&gt;(300, 200)&lt;/code&gt; is toward the bottom-right.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;QPainter coordinates with annotated points&quot; src=&quot;https://www.pythonguis.com/feeds/coordinates-annotated.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This is the expected behavior &amp;mdash; the y-axis points downward.&lt;/p&gt;
&lt;h2 id=&quot;why-does-it-work-this-way&quot;&gt;Why does it work this way?&lt;/h2&gt;
&lt;p&gt;Screen coordinate systems with the origin at the top-left are a convention inherited from early computer displays, where the electron beam in a CRT monitor scanned from the top-left of the screen, line by line, downward. This convention carried forward into virtually all modern windowing and graphics systems, including Qt.&lt;/p&gt;
&lt;h2 id=&quot;converting-from-mathematical-coordinates&quot;&gt;Converting from mathematical coordinates&lt;/h2&gt;
&lt;p&gt;If you're working with data that uses standard mathematical coordinates (origin at the bottom-left, y increasing upward), you'll need to convert the y values before drawing. The formula is straightforward:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;
&lt;span class=&quot;code-block-language code-block-python&quot;&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;y_screen = height - 1 - y_math
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;Where:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;y_screen&lt;/code&gt; is the y coordinate QPainter expects (origin at top-left).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;y_math&lt;/code&gt; is the y coordinate in standard math notation (origin at bottom-left).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;height&lt;/code&gt; is the height of your drawing surface in pixels.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;- 1&lt;/code&gt; is there because pixel coordinates are zero-indexed. A &lt;code&gt;QPixmap&lt;/code&gt; with a height of 300 has valid y coordinates from 0 to 299.&lt;/p&gt;
&lt;p&gt;Let's say you have a canvas that's 300 pixels tall, and you want to draw a line from the mathematical point &lt;code&gt;(10, 10)&lt;/code&gt; to &lt;code&gt;(300, 200)&lt;/code&gt; as if the origin were at the bottom-left. You'd convert each y coordinate:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;
&lt;span class=&quot;code-block-language code-block-python&quot;&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;height = 300

# Mathematical coordinates.
x1, y1_math = 10, 10
x2, y2_math = 300, 200

# Convert y values for screen drawing.
y1_screen = height - 1 - y1_math  # 300 - 1 - 10 = 289
y2_screen = height - 1 - y2_math  # 300 - 1 - 200 = 99

painter.drawLine(x1, y1_screen, x2, y2_screen)
# Equivalent to: painter.drawLine(10, 289, 300, 99)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;Now the line will go from near the &lt;em&gt;bottom&lt;/em&gt;-left upward to the right &amp;mdash; just like you'd expect on a math plot.&lt;/p&gt;
&lt;h2 id=&quot;a-helper-function-for-coordinate-conversion&quot;&gt;A helper function for coordinate conversion&lt;/h2&gt;
&lt;p&gt;If you're doing a lot of drawing with mathematical coordinates, a small helper function keeps things tidy:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;
&lt;span class=&quot;code-block-language code-block-python&quot;&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;def math_to_screen(x, y, height):
    &quot;&quot;&quot;Convert mathematical (bottom-left origin) coordinates
    to screen (top-left origin) coordinates.&quot;&quot;&quot;
    return x, height - 1 - y
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;You can then use it like this:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;
&lt;span class=&quot;code-block-language code-block-python&quot;&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;x1, y1 = math_to_screen(10, 10, canvas_height)
x2, y2 = math_to_screen(300, 200, canvas_height)
painter.drawLine(x1, y1, x2, y2)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 id=&quot;comparing-both-coordinate-systems-side-by-side&quot;&gt;Comparing both coordinate systems side by side&lt;/h2&gt;
&lt;p&gt;This complete example draws the same line using both coordinate systems, so you can see the difference clearly. The left canvas uses QPainter's native coordinates (origin top-left), and the right canvas converts from mathematical coordinates (origin bottom-left).&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;
&lt;span class=&quot;code-block-language code-block-python&quot;&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;import sys

from PyQt6.QtCore import Qt
from PyQt6.QtGui import QPixmap, QPainter, QPen, QFont
from PyQt6.QtWidgets import (
    QApplication, QLabel, QMainWindow, QHBoxLayout, QVBoxLayout, QWidget,
)


def math_to_screen(x, y, height):
    &quot;&quot;&quot;Convert mathematical (bottom-left origin) coordinates
    to screen (top-left origin) coordinates.&quot;&quot;&quot;
    return x, height - 1 - y


def draw_annotated_line(canvas, x1, y1, x2, y2, label_start, label_end):
    &quot;&quot;&quot;Draw a line on a QPixmap with annotated endpoints.&quot;&quot;&quot;
    painter = QPainter(canvas)

    # Draw the line.
    pen = QPen(Qt.blue, 2)
    painter.setPen(pen)
    painter.drawLine(x1, y1, x2, y2)

    # Draw and label the start point.
    painter.setPen(QPen(Qt.red, 6))
    painter.drawPoint(x1, y1)
    painter.setPen(QPen(Qt.black))
    painter.setFont(QFont(&quot;Arial&quot;, 9))
    painter.drawText(x1 + 8, y1 + 5, label_start)

    # Draw and label the end point.
    painter.setPen(QPen(Qt.red, 6))
    painter.drawPoint(x2, y2)
    painter.setPen(QPen(Qt.black))
    painter.drawText(x2 - 80, y2 + 20, label_end)

    painter.end()


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle(&quot;Coordinate System Comparison&quot;)

        canvas_width = 350
        canvas_height = 300

        # --- Left canvas: native QPainter coordinates ---
        canvas_native = QPixmap(canvas_width, canvas_height)
        canvas_native.fill(Qt.white)
        draw_annotated_line(
            canvas_native,
            10, 10, 300, 200,
            &quot;(10, 10)&quot;, &quot;(300, 200)&quot;,
        )

        label_native = QLabel()
        label_native.setPixmap(canvas_native)

        title_native = QLabel(&quot;Screen coordinates\n(origin top-left)&quot;)
        title_native.setAlignment(Qt.AlignCenter)
        title_native.setStyleSheet(&quot;font-weight: bold;&quot;)

        left_layout = QVBoxLayout()
        left_layout.addWidget(title_native)
        left_layout.addWidget(label_native)

        # --- Right canvas: mathematical coordinates converted ---
        canvas_math = QPixmap(canvas_width, canvas_height)
        canvas_math.fill(Qt.white)

        sx1, sy1 = math_to_screen(10, 10, canvas_height)
        sx2, sy2 = math_to_screen(300, 200, canvas_height)
        draw_annotated_line(
            canvas_math,
            sx1, sy1, sx2, sy2,
            f&quot;math(10,10) &amp;rarr; screen({sx1},{sy1})&quot;,
            f&quot;math(300,200) &amp;rarr; screen({sx2},{sy2})&quot;,
        )

        label_math = QLabel()
        label_math.setPixmap(canvas_math)

        title_math = QLabel(&quot;Math coordinates converted\n(origin bottom-left)&quot;)
        title_math.setAlignment(Qt.AlignCenter)
        title_math.setStyleSheet(&quot;font-weight: bold;&quot;)

        right_layout = QVBoxLayout()
        right_layout.addWidget(title_math)
        right_layout.addWidget(label_math)

        # --- Combine both sides ---
        main_layout = QHBoxLayout()
        main_layout.addLayout(left_layout)
        main_layout.addLayout(right_layout)

        container = QWidget()
        container.setLayout(main_layout)
        self.setCentralWidget(container)


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;When you run this, you'll see two canvases side by side. On the left, the line slopes downward from the top-left, which is what QPainter naturally produces. On the right, the same mathematical coordinates have been converted, so the line slopes upward from the bottom-left &amp;mdash; matching what you'd see on a standard math plot.&lt;/p&gt;
&lt;h2 id=&quot;drawing-axes-to-orient-yourself&quot;&gt;Drawing axes to orient yourself&lt;/h2&gt;
&lt;p&gt;When you're experimenting with coordinates, it can help to draw a simple set of axes on your canvas. Here's a quick helper that draws x and y axes with the origin marked:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;
&lt;span class=&quot;code-block-language code-block-python&quot;&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;import sys

from PyQt6.QtCore import Qt
from PyQt6.QtGui import QPixmap, QPainter, QPen, QFont
from PyQt6.QtWidgets import QApplication, QLabel, QMainWindow


def draw_axes(painter, width, height):
    &quot;&quot;&quot;Draw simple x and y axes with labels.&quot;&quot;&quot;
    painter.setPen(QPen(Qt.gray, 1, Qt.DashLine))

    # X-axis along the top (y=0).
    painter.drawLine(0, 0, width - 1, 0)

    # Y-axis along the left (x=0).
    painter.drawLine(0, 0, 0, height - 1)

    # Label the origin.
    painter.setPen(QPen(Qt.darkGray))
    painter.setFont(QFont(&quot;Arial&quot;, 8))
    painter.drawText(5, 15, &quot;(0, 0)&quot;)

    # Label the x direction.
    painter.drawText(width - 60, 15, f&quot;x &amp;rarr; ({width - 1})&quot;)

    # Label the y direction.
    painter.save()
    painter.translate(15, height - 10)
    painter.drawText(0, 0, f&quot;y &amp;darr; ({height - 1})&quot;)
    painter.restore()


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle(&quot;QPainter Axes&quot;)

        canvas_width = 400
        canvas_height = 300

        canvas = QPixmap(canvas_width, canvas_height)
        canvas.fill(Qt.white)

        painter = QPainter(canvas)
        draw_axes(painter, canvas_width, canvas_height)

        # Draw some points to see where they land.
        points = [
            (50, 50),
            (200, 150),
            (350, 250),
            (350, 50),
            (50, 250),
        ]

        painter.setPen(QPen(Qt.red, 6))
        for x, y in points:
            painter.drawPoint(x, y)

        painter.setPen(QPen(Qt.black))
        painter.setFont(QFont(&quot;Arial&quot;, 9))
        for x, y in points:
            painter.drawText(x + 6, y - 6, f&quot;({x}, {y})&quot;)

        painter.end()

        label = QLabel()
        label.setPixmap(canvas)
        self.setCentralWidget(label)


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;This draws the axes along the top and left edges of the canvas and plots several points with their coordinates labeled. It's a great way to build intuition about where things will appear.&lt;/p&gt;
&lt;h2 id=&quot;valid-coordinate-ranges&quot;&gt;Valid coordinate ranges&lt;/h2&gt;
&lt;p&gt;One more thing to keep in mind: pixel coordinates on a &lt;code&gt;QPixmap&lt;/code&gt; are zero-indexed. If you create a pixmap with:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;
&lt;span class=&quot;code-block-language code-block-python&quot;&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;canvas = QPixmap(400, 300)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;Then the valid coordinate ranges are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;x&lt;/strong&gt;: 0 to 399 (that's &lt;code&gt;width - 1&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;y&lt;/strong&gt;: 0 to 299 (that's &lt;code&gt;height - 1&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Drawing outside these ranges won't cause an error, but anything beyond the edges simply won't be visible.&lt;/p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;
&lt;p&gt;The QPainter coordinate system places &lt;code&gt;(0, 0)&lt;/code&gt; at the top-left of the drawing surface, with x increasing to the right and y increasing downward. This is standard across virtually all screen-based graphics systems.&lt;/p&gt;
&lt;p&gt;If you need to work with mathematical coordinates where &lt;code&gt;(0, 0)&lt;/code&gt; is at the bottom-left and y increases upward, you can convert using the formula:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;
&lt;span class=&quot;code-block-language code-block-python&quot;&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;y_screen = height - 1 - y_math
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;Once you've internalized this, drawing with QPainter becomes predictable. When in doubt, drop some annotated points on your canvas &amp;mdash; seeing the coordinates labeled right next to the dots is the fastest way to confirm everything is landing where you expect.&lt;/p&gt;
&lt;p&gt;For more details on Qt's coordinate system, take a look at the &lt;a href=&quot;https://doc.qt.io/qt-5/coordsys.html&quot;&gt;official Qt coordinate system documentation&lt;/a&gt;.&lt;/p&gt;
            &lt;p&gt;For an in-depth guide to building Python GUIs with PyQt6 see my book, &lt;a href=&quot;https://www.pythonguis.com/pyqt6-book/&quot;&gt;Create GUI Applications with Python &amp;amp; Qt6.&lt;/a&gt;&lt;/p&gt;</description>
	<pubDate>Wed, 02 Sep 2026 06:00:00 +0000</pubDate>
</item>
<item>
	<title>Mark Dufour: Shed Skin restricted-Python-to-C++ compiler v0.9.13 released</title>
	<guid>http://shed-skin.blogspot.com/2026/09/shed-skin-restricted-python-to-c.html</guid>
	<link>http://shed-skin.blogspot.com/2026/09/shed-skin-restricted-python-to-c.html</link>
	<description>&lt;p&gt;I have just released &lt;a href=&quot;https://github.com/shedskin/shedskin/releases/tag/v0.9.13&quot;&gt;version 0.9.13&lt;/a&gt; of &lt;a href=&quot;https://github.com/shedskin/shedskin&quot;&gt;Shed Skin&lt;/a&gt;, a restricted-python to C++ transpiler. Shed Skin allows one to effectively convert (or transpile) pure Python code to highly optimized machine code. This comes at the cost though of having to conform to a seriously restricted subset of Python features/libraries. Programs currently also cannot be too large, although it is possible to generate extension modules, that can be used in larger programs.
  
&lt;p&gt; The following screenshot is of a &lt;a href=&quot;https://github.com/shedskin/shedskin/tree/main/examples/doom&quot;&gt;DOOM engine&lt;/a&gt;, that becomes more than 30 times faster after transpilation. The engine is compiled with Shed Skin, then imported in a larger program that uses Pygame for the UI (see also a &lt;a href=&quot;https://www.youtube.com/watch?v=171AQx7l43s&quot;&gt;before-after video&lt;/a&gt;).
  
  &lt;div class=&quot;separator&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgZf4Sb2zkaEg1_jinY3n0dy113nTc18kPAjxoAbUyvVXHrsFY18QsNs7Q7IdOzt6rfDIcqaUz50iP-ZCbfzRidbgigeQqQRMbRv1V-0MLPo0VfA9g365Th6aBw0ccK9_1UNMzpgSjfb-FPmKMyuOYKe-LFzuwMDQUrI5_gvYYuDoE_dqMThyphenhyphenZ5tQ/s1201/doom.png&quot;&gt;&lt;img alt=&quot;&quot; border=&quot;0&quot; width=&quot;320&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgZf4Sb2zkaEg1_jinY3n0dy113nTc18kPAjxoAbUyvVXHrsFY18QsNs7Q7IdOzt6rfDIcqaUz50iP-ZCbfzRidbgigeQqQRMbRv1V-0MLPo0VfA9g365Th6aBw0ccK9_1UNMzpgSjfb-FPmKMyuOYKe-LFzuwMDQUrI5_gvYYuDoE_dqMThyphenhyphenZ5tQ/s320/doom.png&quot; /&gt;&lt;/a&gt;&lt;/div&gt;

&lt;p&gt;Version 0.9.13 is the first version that was heavily improved by the use of AI (more specifically, Claude - thanks to Shakeeb for starting this). It was able to spot (and fix) many bugs, especially in the C++ backend, but also test gaps. It also helped to add support for many missing features in Python 3.15. We are now actually pretty close to full compatibility there, at least with regards to the supported modules and everything that is compatible with Shed Skin (no os.walk yet, for example, as it uses heterogenous tuples of length &gt; 2, something we may fix for 0.9.14).
  
&lt;p&gt;The use of AI should make it much easier to contribute as well. For example, if your program doesn't work, AI can minimize it and produce a useful bug report to submit to the project. It can even start to try and fix the problem, create new tests and so on. Or otherwise look for gaps/bugs to work on.&lt;/p&gt;&lt;/p&gt;&lt;/p&gt;&lt;/p&gt;</description>
	<pubDate>Wed, 02 Sep 2026 00:16:44 +0000</pubDate>
</item>

</channel>
</rss>
