19 Jun 2026

feedDjango community aggregator: Community blog posts

Issue 342: DSF Executive Director Search

## News

Announcing the Search for a DSF Executive Director

The Django Software Foundation is hiring its first Executive Director, and we have the Django community to thank for making it possible.

Six Django web development agencies have jointly pledged $47,500 to help fund the Executive Director's first year: Caktus Group, Lincoln Loop, Six Feet Up, Cuttlesoft, OddBird, and Two Rock. This is the financial foundation we needed to move from "we should hire an ED someday" to "we are hiring an ED now."

I'm delighted to rejoin the Sovereign Tech Fellowship

Hugo van Kemenade returns to the Sovereign Tech Fellowship after being one of six participants in the 2025 pilot, calling out how dedicated time helped ship Python 3.14 and 3.15 releases, mentor triagers, and improve release automation and accessibility. The post also tracks a wide set of community and governance work, and looks ahead to a larger 2026 cohort spanning maintainers, community managers, and technical writers.


Python Software Foundation

Python Software Foundation News: PSF Board Election Dates for 2026

PSF Board elections for 2026 open for nominations on July 28 (2:00 pm UTC) and voting runs September 1 to September 15, with voter affirmation due August 25. The Packaging Council election will run in parallel under PEP 772, and PSF member voting eligibility is handled via psfmember.org.


Updates to Django

Last week we had 24! pull requests merged into Django by 11 different contributors.

This week's Django highlights ๐Ÿฆ„:


Sponsored Link

Reach 4,300+ Engaged Django Developers

Sponsor this newsletter to reach an active community of Python and Django developers.

Django Pony reading the news
The #1 Django Newsletter

Articles

In search of a new contribution model

Carlton Gibson on why open source's contribution model is broken--burnout, extractive contributions, harassment, and now AI--and his plans to experiment with something less open-by-default on newer projects.

The University In The AI Era

From Carson Gross, creator of HTMX and full-time college professor, a detailed and practical look at what AI means for universities in general and computer science programs in particular.

How I Work From Anywhere Without Losing My Place

Jeff has been running a new remote dev setup that allows for seamless switching between home office, an iPad, or even a phone when out on the go.

LLM-Inspired Development

How a bad idea from an LLM led to a good idea on a website.

Tech doesn't matter? Why to use Django for agentic coding

Ronny Vedrilla argues that in the age of agentic coding, Django's opinionated structure, secure-by-default posture, and heavy representation in training data make it an ideal "harness" that keeps AI agents on the rails-not a competitive edge, but a hedge against shipping a quiet disaster.


Videos

The Modern Python Web Stack: Django, FastAPI, uv, Pydantic, and AI

A 5-minute conversation from PyCon US with Jeff Triplett on how Python web development is changing fast. (Yes, this video features Jeff and Will, the two authors of this newsletter, but we still think it warrants mention! ๐Ÿค)


Podcasts

Teaching Python #158: Will Vincent on Django, AI Coding, and Why Fundamentals Still Matter

A chat on why Django continues to matter, the reality behind vibe coding, local AI models, and more.


Django Forum

Call for mentors - GSoC 2026 with Django!

Google Summer of Code is around the corner and there is still a need for mentors on some projects.

Ticket 34753, Document how to properly escape to in email messages

An active discussion around this particular issue. Checking the forum is a great way to get a pulse on what's happening with core Django development.


Django Fellow Reports

Jacob Walls

In this four-day week (I headed out Friday for a college reunion), everything got a little bit better. First, check out @blighj's estimate showing that collectstatic's import statement detection reliability (needed to rewrite URLs) improves in Django 6.1 from 88% to 99%. Meanwhile @felixxm is stress-testing database defaults and landing fixes needed for using Django 6.1's UUID4()/UUID7() functions. Finally, we made the test client more friendly for third-party permission packages like django-guardian and django-rules. @sage also spotted a breakage in DRF in the upcoming Django 6.1 beta, since Wagtail tests against Django's main branch. I expect the fix to land before the beta is even out. Be like wagtail and test main!

Natalia Bidart

This week had a bit of a reset feel to it ๐Ÿงน. After the previous stretch of PyCon US, security prep, and the security release itself ๐Ÿ, I spent time going through pending and snoozed items โฐ, trying to close loops and get things back to a more manageable state.

We also reviewed and triaged a batch of security reports ๐ŸŽ that were shared by a major AI company, following conversations I had at PyCon US ๐Ÿ ๐Ÿ–๏ธ about the growing volume of LLM-generated security submissions and the challenges they create for OSS projects (Django in particular). The reports were generated using an advanced security-focused model ๐Ÿค– against the Django codebase. We evaluated each finding, confirming and addressing valid issues where appropriate and mapping others to existing tickets and prior reports. Overall, Django is in good shape ๐Ÿ’ช, as the results largely overlapped with known reports, validated our current triage approach, and reinforced confidence in our security stance ๐Ÿ‘.


Events

Django Girls Krakow on 18th July 2026

This event is taking place during EuroPython at the sprints venue.

Django Day Copenhagen 2026

Djangonauts from in and around Denmark are meeting up for the second edition of Django Day Copenhagen 2026, October 2.

International Travel to DjangoCon US 2026

Are you attending DjangoCon US 2026 in Chicago, Illinois, but you are not from US and need some travel information? Here are some things to consider when planning your trip.

Join DEFNA! There's a seat on the DEFNA board open

Django Events Foundation North America (DEFNA) is looking for another board member. We have eight board members currently and are looking for another person passionate about growing the DjangoCon US community to join.


Django Job Board

Senior Python/Django Developer at Gryps ๐Ÿ†•

Founding ML/Data Scientist (Remote, UK) at MyDataValue


Projects

ranahaani/GNews

A Happy and lightweight Python Package that Provides an API to search for articles on Google News and returns a JSON response.

jazzband/django-newsletter

An email newsletter application for the Django web application framework, including an extended admin interface, web (un)subscription, dynamic e-mail templates, an archive and HTML email support.

19 Jun 2026 3:00pm GMT

feedPlanet Python

Core Dispatch: Core Dispatch #6

Welcome back to Core Dispatch! This edition covers June 4 through 19, 2026. Python 3.14.6 and 3.13.14 landed on June 10, and the next milestone is 3.15.0 beta 3 on June 23.

The big news this fortnight comes from the Steering Council, who put out an announcement on the path forward for the experimental JIT. The JIT entered CPython's main branch as an experiment, alongside the Informational PEP 744. The Council would like to see its path forward worked out through a Standards Track PEP, giving the project the explicit, structured conversation it hasn't really had yet about what people expect from a JIT, including performance targets, interop guarantees, and tooling compatibility.

On a related note, JIT contributors have opened a thread to gather community perspectives on the JIT as they begin drafting that PEP. Give it a read, and if you've got experiences, expectations, or concerns to share, it's a good place to weigh in.

It's been a bit quieter on the PEP front over the past two weeks, though PEP 835, a shorthand syntax for Annotated type metadata, was newly drafted.

Over on the PSF side, the Board has published the draft of its 2026 strategic plan: six organizational goals and four program goals spanning financial sustainability, supply chain security, and community empowerment. The feedback window is open through June 25, so if you've got thoughts, now's the time. The 2026 PSF Board election dates are out too.

As always, if you maintain a package or just like living on the edge, give the latest 3.15 beta a spin and file any issues you find.

Upcoming Releases

Official News

PEP Updates

Steering Council Updates

Merged PRs

Discussion

Core Dev Musings

Upcoming CFPs & Conferences

Community

Credits

19 Jun 2026 12:00am GMT

Bob Belderbos: End-to-End Testing Every Rust Exercise with Playwright

The Rust platform has 71 exercises and counting (I just added a new track of Unix exercises). They all share the same interface: load an editor, type code, validate it against a Rust backend. When I make any changes to the platform, how do I confirm nothing breaks? Enter end-to-end testing with Playwright.

The Problem

Manual testing doesn't scale. Every time I add an exercise, tweak the editor, or update the validation flow, I need confidence that all exercises still work. Not just that the page loads, but the full loop: login, navigate, type code, submit, see results.

Unit tests cover the Django app, and the Rust validator has its own test suite. But neither exercises the full path a student takes: loading an exercise and getting a real pass or fail back.

One Test Function, 71 Test Cases

Playwright with pytest covers this in under 50 lines. Here's the core test file:

import psycopg2
import pytest
from decouple import config

from .constants import DOMAIN

exercises = []
with psycopg2.connect(dsn=config("DATABASE_URL")) as conn:
    with conn.cursor() as cursor:
        cursor.execute(
            "SELECT slug, solution FROM bites_exercise WHERE public = true"
        )
        exercises = cursor.fetchall()


@pytest.mark.parametrize("exercise", exercises, ids=[ex[0] for ex in exercises])
def test_exercise(logged_in_page, exercise):
    slug, solution = exercise
    page = logged_in_page

    exercise_url = f"{DOMAIN}/{slug}"
    page.goto(exercise_url)
    page.wait_for_url(exercise_url)

    page.wait_for_selector(".CodeMirror", state="visible")
    page.wait_for_function(
        "document.querySelector('.CodeMirror')?.CodeMirror !== undefined"
    )

    page.evaluate(
        f"""document.querySelector('.CodeMirror').CodeMirror.setValue({repr(solution)})"""
    )
    page.click("#validate-button")

    page.wait_for_function(
        "document.querySelector('#feedback').innerText.includes('Congrats') || "
        "document.querySelector('#feedback').innerText.includes('Oops')",
        timeout=30000,
    )

    validate_result = page.text_content("#feedback")
    assert "Congrats, you passed this exercise" in validate_result

The database query at module load fetches every public exercise with its solution. @pytest.mark.parametrize turns that into 71 test cases. Each test navigates, injects the solution, validates, and asserts success.

Here is Playwright running the tests locally against the real Rust validator, one exercise after another:

Your browser does not support embedded video.

Patterns That Made It Work

Session-scoped fixtures for speed

Launching a browser is expensive. Logging in is expensive. Do it once:

@pytest.fixture(scope="session")
def browser(e2e_user):
    with sync_playwright() as p:
        with p.chromium.launch(headless=HEADLESS) as browser:
            yield browser


@pytest.fixture(scope="session")
def logged_in_page(browser):
    page = browser.new_page()
    page.set_default_timeout(30_000)
    page.goto(f"{DOMAIN}/pbadmin/")
    page.fill('input[name="username"]', LOGIN)
    page.fill('input[name="password"]', PASSWORD)
    page.click('input[type="submit"]')
    yield page

All 71 exercises run against the same authenticated browser session, so the login cost is paid once.

Waiting for CodeMirror

One tricky thing with Playwright is timing. Sometimes elements are not yet ready when you hit the page. In this case you have to wait for the CodeMirror editor to be fully initialized before injecting code:

page.wait_for_selector(".CodeMirror", state="visible")
page.wait_for_function(
    "document.querySelector('.CodeMirror')?.CodeMirror !== undefined"
)

page.evaluate(
    f"""document.querySelector('.CodeMirror').CodeMirror.setValue({repr(solution)})"""
)

First we wait for the selector to be visible, then we wait for the JavaScript instance to be ready.

Avoiding Django's async context trap

Another issue I faced was creating a Django user inside a Playwright fixture triggered:

SynchronousOnlyOperation: You cannot call this from an async context

I worked around it by creating the test user in a separate fixture that runs before Playwright starts:

@pytest.fixture(scope="session")
def e2e_user(django_db_blocker):
    with django_db_blocker.unblock():
        return ensure_e2e_user()


@pytest.fixture(scope="session")
def browser(e2e_user):  # e2e_user runs first
    with sync_playwright() as p:
        ...

The django_db_blocker.unblock() context manager allows database access in session-scoped fixtures. Order matters: the user must exist before the browser fixture runs.

Running Locally vs CI

The test suite runs against a live database with the Rust validator running. That's deliberate: I want real integration testing, not mocked responses.

# Run all 71 exercises
uv run pytest tests/test_e2e.py -v

# Debug a specific exercise
HEADLESS=False uv run pytest tests/test_e2e.py -v -k "exercise-slug"

By default, the tests run headless, which means no browser window opens. This is faster and works well in CI. If you want to see what Playwright is doing, set HEADLESS=False to open a visible browser window.

This is essential for debugging why a particular exercise fails. Use the -k option to filter for a specific exercise by slug. And you can use --pdb to leave the browser window open when a test fails, so you can inspect the state.

For CI, I run unit tests only. E2E tests require the Rust backend and take longer. I run them locally before pushing major changes. This is a good example of separating unit and integration tests as mentioned in How to Tell if Your Python Mock Is Actually Working.

What this buys me

Add an exercise, it's tested automatically, no new test code. That's the whole point of parameterizing over the database instead of hand-writing cases. Every frontend change now runs through a regression suite before it reaches users.

I find Playwright more modern and ergonomic than Selenium; the one rough edge is element timing, which the wait_for_function calls above handle. If you're testing anything with dynamic content, parameterizing over your real data beats writing one test per case.

19 Jun 2026 12:00am GMT

18 Jun 2026

feedPlanet Python

Ned Batchelder: Dodecahedron with stars

I saw this dodecahedron with an Islamic-inspired pattern designed by Taj Ragoo. As soon as I saw it, I knew I had to make one. I studied the pattern, wrote some Python, and made myself a PDF. I cut it out, folded it, glued it together, and now I have one of my own:

A paper dodecahedron on a cluttered desk

I love that this elegantly combines two pure geometric forms: the Platonic dodecahedron (12 uniform pentagons), and an Islamic pattern using five-pointed stars.

Looking closely, details emerge:

The dodecahedron with some regions of the pattern highlighted in colors

Each face has ten small stars in a ring. I've lightened them a bit in the front face here. At the center of each face is a ten-pointed star (highlighted in red), made of two overlaid five-pointed stars.

The real genius of the pattern is at the corners. I've highlighted one in blue. It's a star made of the same parts as the central ten-pointed star, but there are only nine points. It works because three pentagons lying flat touching at a point occupy 324 degrees, leaving a 36-degree gap.

When the dodecahedron is folded together, the gap is closed. 36 degrees is exactly one-tenth of a complete 360-degree circle, so exactly one point of the ten-pointed star is missing, leaving a perfect nine-pointed star using the same shapes, spread over the corners of three pentagons. Beautiful!

If this appeals to you, follow Taj on Instagram: he's got more Platonic/Islamic mashups to enjoy. The paper versions are just prototypes of the final versions he makes in wood.

Of course, you can get my PDF and make one for yourself:

A thumbnail of the PDF

The Python code to draw the net isn't great: it has no real parallels to the structure of each face. It's a lot of math and line drawing to get things in the right places. My ideal would be to have a toolset that used a tile-placing abstraction, to be able to do more interesting designs. Some day.

It was a joy to work on this though. It was a slow process of studying the original, working out the math, then mulling over coding approaches. The code was developed in small steps over weeks. Then printing initial versions, marking them up, working out the tab structure. Some copies were colored to understand how the lines flowed across the whole dodecahedron. It was good to be working in both the mental and physical worlds:

Various stages of progress in a messy pile

Update: it looks like the design was originally by Dana Awartani: Dodecahedron Within an Icosahedron.

18 Jun 2026 10:15am GMT

17 Jun 2026

feedDjango community aggregator: Community blog posts

The 2026 way of using importmaps in Django

The 2026 way of using importmaps in Django

I last wrote about Django, JavaScript modules and importmaps in May 2025, slightly over a year ago.

The main topic of this post is the django-js-asset 4.0 release. The library is used in many places, some of the more well-known packages using it are django-mptt and django-ckeditor. I have since done a lot of work evolving the ways of integrating importmaps but the efforts to standardize upon an approach have stalled a bit. The main reason for this, apart from time and energy, was that I wasn't really all that happy with the global importmap. When I had only a few modules using the importmap facility, I didn't care all that much. Now that the recently released django-content-editor 9.0 also uses importmaps for shipping a refactored, much more modular JavaScript implementation while still keeping all the benefits of cache busting using ManifestStaticFilesStorage1, having a global importmap got annoying. The content editor JavaScript is only used within the Django administration interface, but when using a single global importmap object, the importmap entries were always there on each page that used an importmap at all.

A better solution was needed. I'm a big fan of using forms.Media for collecting CSS and JavaScript from widgets, forms and utilities. It helps me avoid inline JavaScript since at least 2017. I'm not using it for site-wide CSS and JavaScript, I'm still transpiling, PostCSS-ing and bundling the assets using rspack as for example written about here and here.

Why importmaps?

A quick refresher on why this matters at all. Django's ManifestStaticFilesStorage hashes the contents of each file into its name for cache busting, but out of the box it doesn't rewrite the import statements inside JavaScript modules. Importmaps bridge the gap: your code imports a stable name:

import { initializeEditors } from "django-prose-editor/editor"

and the importmap tells the browser where that name actually lives:

<script type="importmap">
{"imports": {
  "django-prose-editor/editor": "/static/django_prose_editor/editor.6e8dd4c12e2e.js"
}}
</script>

So the import stays clean and constant while the file behind it can get a new hash on every deploy.

django-js-asset 4.0

The updated django-js-asset 4.0 doesn't ship the old, global importmap at all. This means the upgrade might require some work. Instead of one importmap shared across the whole site, you now get a specific importmap assembled for the context at hand - either by Django itself when it collects the media of your forms, widgets and the admin, or explicitly by you in a view or context processor. The building block in both cases is the ImportMap object; when it travels through js_asset.Media (a subclass of django.forms.Media) the maps are automatically merged into a single <script type="importmap">, by customizing and extending what Django does already when merging media instances.

The release notes go into more detail.

In practice

If you're using a package such as django-prose-editor in the Django admin you don't have to do anything, things should just work.

If you're using such a package outside the admin, you have to remove "js_asset.context_processors.importmap" from your list of context processors. On one particular website the prose editor is the only package with importmap entries outside the admin, so I have to add the importmap to the template context myself:

from django_prose_editor.widgets import importmap

def view(request, ...):
    return render(request, "template.html", {
        # ...
        "importmap": importmap,
    })

The template then just renders it in the <head>:

... {{ importmap }}</head>

On a different site, I have a slightly more involved scenario where I previously used importmap.update(...) to add my own entries to the importmap. There, I'm using a custom context processor to always add these entries to the importmap too:

from django_prose_editor.widgets import importmap as dpe_importmap
from js_asset import ImportMap, static_lazy

_site_importmap = ImportMap({
    "imports": {
        "my-module": static_lazy("my-module.js"),
    }
})
_importmap = dpe_importmap | _site_importmap

def importmap(request):
    return {"importmap": _importmap}

This importmap is merged once at server startup and then served repeatedly to the client. Because we use the lazy version of the static function we can do this during startup and not worry about files not yet collected by collectstatic - we'll get the correct paths later.

On the same site as the previous example, I also have an admin inline which requires some JavaScript and also an importmap:

from django.contrib import admin
from django.forms import Script
from js_asset import Media, ImportMap

# Initializing this once. Not necessary but I like it better that way.
_importmap = ImportMap({
    "imports": {
        # ...
    }
})

class ModelInline(admin.StackedInline):
    @property
    def media(self):
        return Media(
            js=[
                _importmap,
                Script("module.js", type="module"),
            ]
        )

As of 4.0, JS and CSS produce Django's own Script and Stylesheet objects, so you can import and use Script directly from django.forms as shown above (on Django 4.2-5.1, import it from js_asset instead, which backports it). The familiar JS("module.js", {"type": "module"}) wrapper still works unchanged if you prefer it - it just takes a positional dict instead of keyword arguments.

Here, it's really important to use the js_asset.Media and not django.forms.Media. js_asset.Media knows how to handle importmaps - all importmaps are collected from all media lists, merged and added to the output before all other CSS and especially JavaScript. The reason for that is that browsers only honour a single importmap per page, and it really has to appear before all JavaScript modules referencing any entries in the importmap.

The nice thing about js_asset.Media is that it doesn't have to appear first in the list of media classes which are merged - it can also appear in the middle or last, and still can do its magic after all Media objects have been merged into a single one.

The rest is handled by Django itself, since it already supports collecting media assets. The missing piece was just the importmap object and the js_asset.Media class which knows how to special case them, and which - through the power of overriding __add__ and __radd__ takes over all the other media instances.

What's next

I haven't yet used CSP nonces using {% csp_nonce_attr media %} in production myself, but it should just work, even with importmaps and everything else. Given that I have a passing test suite I have no reason to believe it doesn't already work, but I'd like to have a confirmation.

I'm hoping to standardize some more. If we could get something like this in Django core that would be really nice. Maybe I'll be able to work on that at Django on the Med ๐Ÿ–๏ธ. Since no browser supports multiple importmaps as of today having multiple implementations of importmaps in the Django ecosystem will lead to trouble down the road. I think there is a clear case to be made for importmap support in Django and I would obviously love it if the approach implemented today in django-js-asset would be the basis for the official solution.


  1. Without having to do any overrides to enable ESM support. โ†ฉ

17 Jun 2026 5:00pm GMT

16 Jun 2026

feedDjango community aggregator: Community blog posts

Cheating as a programming discipline

Great programmers cheat. A hard problem gets quietly swapped for an easier one; a transaction-grade database is replaced by a flat file nobody misses; machinery everyone else considers mandatory simply never gets built. They know a lot - and that's exactly why they get away with it.

Cheating as a programming discipline

16 Jun 2026 11:00am GMT

09 Jun 2026

feedPlanet Twisted

Hynek Schlawack: How to Ditch Codecov for Python Projects

Codecov's unreliability breaking CI on my open source projects has been a constant source of frustration for me for years. I have found a way to enforce coverage over a whole GitHub Actions build matrix that doesn't rely on third-party services.

09 Jun 2026 12:00am GMT

22 May 2026

feedPlanet Twisted

Glyph Lefkowitz: Opaque Types in Python

Let's say you're writing a Python library.

In this library, you have some collection of state that represents "options" or "configuration" for a bunch of operations. Such a set of options is a bundle of potentially ever-increasing complexity. Thus, you will want it to have an extremely minimal compatibility surface, with a very carefully chosen public interface, that is either small, or perhaps nothing at all. Such an object conveys state and might have some private behavior, but all you want consumers to be able to do is build it in very constrained, specific ways, and then pass it along as a parameter to your own APIs.

By way of example, imagine that you're wrapping a library that handles shipping physical packages.

There are a zillion ways to do it ship a package. There are different carriers who can ship it for you. There's air freight, and ground freight, and sea freight. There's overnight shipping. There's the option to require a signature. There's package tracking and certified mail. Suffice it to say, lots of stuff.

If you are starting out to implement such a library, you might need an object called something like ShippingOptions that encapsulates some of this. At the core of your library you might have a function like this:

1
2
3
4
5
async def shipPackage(
        how: ShippingOptions,
        where: Address,
    ) -> ShippingStatus:
    ...

If you are starting out implementing such a library, you know that you're going to get the initial implementation of ShippingOptions wrong; or, at the very least, if not "wrong", then "incomplete". You should not want to commit to an expansive public API with a ton of different attributes until you really understand the problem domain pretty well.

Yet, ShippingOptions is absolutely vital to the rest of your library. You'll need to construct it and pass it to various methods like estimateShippingCost and shipPackage. So you're not going to want a ton of complexity and churn as you evolve it to be more complex.

Worse yet, this object has to hold a ton of state. It's got attributes, maybe even quite complex internal attributes that relate to different shipping services.

Right now, today, you need to add something so you can have "no rush", "standard" and "expedited" options. You can't just put off implementing that indefinitely until you can come up with the perfect shape. What to do?

The tool you want here is the opaque data type design pattern. C is lousy with such things (FILE, pthread_*_t, fd_set, etc). A typedef in a header file can easily achieve this.

But in Python, if you expose a dataclass - or any class, really - even if you keep all your fields private, the constructor is still, inherently, public. You can make it raise an exception or something, but your type checker still won't help your users; it'll still look like it's a normal class.

Luckily, Python typing provides a tool for this: typing.NewType.

Let's review our requirements:

  1. We need a type that our client code can use in its type annotations; it needs to be public.
  2. They need to be able to consruct it somehow, even if they shouldn't be able to see its attributes or its internal constructor arguments.
  3. To express high-level things (like "ship fast") that should stay supported as we add more nuanced and complex configurations in the future (like "ship with the fastest possible option provided by the lowest-cost carrier that supports signature verification").

In order to solve these problems respectively, we will use:

  1. a public NewType, which gives us our public name...
  2. which wraps a private class with entirely private attributes, to give us an actual data structure, while not exposing the constructor,
  3. a set of public constructor functions, which returns our NewType.

When we put that all together, it looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from dataclasses import dataclass
from typing import Literal, NewType

@dataclass
class _RealShipOpts:
    _speed: Literal["fast", "normal", "slow"]

ShippingOptions = NewType("ShippingOptions", _RealShipOpts)

def shipFast() -> ShippingOptions:
    return ShippingOptions(_RealShipOpts("fast"))

def shipNormal() -> ShippingOptions:
    return ShippingOptions(_RealShipOpts("normal"))

def shipSlow() -> ShippingOptions:
    return ShippingOptions(_RealShipOpts("slow"))

As a snapshot in time, this is not all that interesting; we could have just exposed _RealShipOpts as a public class and saved ourselves some time. The fact that this exposes a constructor that takes a string is not a big deal for the present moment. For an initial quick and dirty implementation, we can just do checks like if options._speed == "fast" in our shipping and estimation code.

However, the main thing we are doing here is preserving our flexibility to evolve the related APIs into the future, so let's see how we might do that. For example, let's allow the shipping options to contain a concrete and specific carrier and freight method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from dataclasses import dataclass
from enum import Enum, auto
from typing import NewType

class Carrier(Enum):
    FedEx = auto()
    USPS = auto()
    DHL = auto()
    UPS = auto()

class Conveyance(Enum):
    air = auto()
    truck = auto()
    train = auto()

@dataclass
class _RealShipOpts:
    _carrier: Carrier
    _freight: Conveyance

ShippingOptions = NewType("ShippingOptions", _RealShipOpts)

def shipFast() -> ShippingOptions:
    return ShippingOptions(_RealShipOpts(Carrier.FedEx, Conveyance.air))

def shipNormal() -> ShippingOptions:
    return ShippingOptions(_RealShipOpts(Carrier.UPS, Conveyance.truck))

def shipSlow() -> ShippingOptions:
    return ShippingOptions(_RealShipOpts(Carrier.USPS, Conveyance.train))

def shippingDetailed(
    carrier: Carrier, conveyance: Conveyance
) -> ShippingOptions:
    return ShippingOptions(_RealShipOpts(carrier, conveyance))

As a NewType, our public ShippingOptions type doesn't have a constructor. Since _RealShipOpts is private, and all its attributes are private, we can completely remove the old versions.

Anything within our shipping library can still access the private variables on ShippingOptions; as a NewType, it's the same type as its base at runtime, so it presents minimal1 overhead.

Clients outside our shipping library can still call all of our public constructors: shipFast, shipNormal, and shipSlow all still work with the same (as far as calling code knows) signature and behavior.

If you need to build and convey some state within your public API, while avoiding breakages associated with compatibility churn, hopefully this technique can help you do that!


Acknowledgments

Thanks for reading, and thank you to my patrons 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 various open-source endeavors, you can support my work as a sponsor.


  1. The overhead is minimal, but it is not completely zero. The suggested idiom for converting to a NewType is to call it like a function, as I've done in these examples, but if you are wanting to use this pattern inside of a hot loop, you can use # type: ignore[return-value] comments to avoid that small cost. โ†ฉ

22 May 2026 12:33am GMT

04 Apr 2026

feedPlanet Twisted

Donovan Preston: Using osascript with terminal agents on macOS

Here is a useful trick that is unreasonably effective for simple computer use goals using modern terminal agents. On macOS, there has been a terminal osascript command since the original release of Mac OS X. All you have to do is suggest your agent use it and it can perform any application control action available in any AppleScript dictionary for any Mac app. No MCP set up or tools required at all. Agents are much more adapt at using rod terminal commands, especially ones that haven't changed in 30 years. Having a computer control interface that hasn't changed in 30 years and has extensive examples in the Internet corpus makes modern models understand how to use these tools basically Effortlessly. macOS locks down these permissions pretty heavily nowadays though, so you will have to grant the application control permission to terminal. But once you have done that, the range of possibilities for commanding applications using natural language is quite extensive. Also, for both Safari and chrome on Mac, you are going to want to turn on JavaScript over AppleScript permission. This basically allows claude or another agent to debug your web applications live for you as you are using them.In chrome, go to the view menu, developer submenu, and choose "Allow JavaScript from Apple events". In Safari, it's under the safari menu, settings, developer, "Allow JavaScript from Apple events". Then you can do something like "Hey Claude, would you Please use osascript to navigate the front chrome tab to hacker news". Once you suggest using OSA script in a session it will figure out pretty quickly what it can do with it. Of course you can ask it to do casual things like open your mail app or whatever. Then you can figure out what other things will work like please click around my web app or check the JavaScript Console for errors. Another very important tips for using modern agents is to try to practice using speech to text. I think speaking might be something like five times faster than typing. It takes a lot of time to get used to, especially after a lifetime of programming by typing, but it's a very interesting and a different experience and once you have a lot of practice It starts to to feel effortless.

04 Apr 2026 1:31pm GMT